homebridge-netatmo-temperature 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,40 @@
1
+ {
2
+ "pluginAlias": "NetatmoTemperature",
3
+ "pluginType": "platform",
4
+ "singular": true,
5
+ "headerDisplay": "Netatmo Weather Station temperatures for HomeKit. Get your credentials at https://dev.netatmo.com",
6
+ "schema": {
7
+ "type": "object",
8
+ "properties": {
9
+ "clientId": {
10
+ "title": "Client ID",
11
+ "type": "string",
12
+ "required": true,
13
+ "description": "Client ID from your Netatmo app at dev.netatmo.com"
14
+ },
15
+ "clientSecret": {
16
+ "title": "Client Secret",
17
+ "type": "string",
18
+ "required": true,
19
+ "description": "Client Secret from your Netatmo app at dev.netatmo.com"
20
+ },
21
+ "refreshToken": {
22
+ "title": "Refresh Token",
23
+ "type": "string",
24
+ "description": "OAuth2 Refresh Token from Netatmo (auto-renews access tokens)"
25
+ },
26
+ "token": {
27
+ "title": "Access Token (optional)",
28
+ "type": "string",
29
+ "description": "Direct access token - use if you don't have a refresh token yet"
30
+ },
31
+ "pollingInterval": {
32
+ "title": "Polling Interval (seconds)",
33
+ "type": "integer",
34
+ "default": 300,
35
+ "minimum": 60,
36
+ "description": "How often to refresh temperature data (default: 300s = 5 min)"
37
+ }
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: (api: API) => void;
2
+ export = _default;
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ module.exports = (api) => {
3
+ api.registerPlatform(PLATFORM_NAME, NetatmoPlatform);
4
+ };
@@ -0,0 +1,24 @@
1
+ export interface NetatmoModule {
2
+ id: string;
3
+ name: string;
4
+ type: string;
5
+ temperature: number | null;
6
+ humidity: number | null;
7
+ co2?: number | null;
8
+ pressure?: number | null;
9
+ noise?: number | null;
10
+ battery?: number | null;
11
+ reachable: boolean;
12
+ }
13
+ export declare class NetatmoApi {
14
+ private client;
15
+ private accessToken;
16
+ private refreshToken;
17
+ private clientId;
18
+ private clientSecret;
19
+ private tokenExpiry;
20
+ private log;
21
+ constructor(clientId: string, clientSecret: string, refreshToken: string, log: Logger, directToken?: string);
22
+ authenticate(): Promise<void>;
23
+ getStationData(): Promise<NetatmoModule[]>;
24
+ }
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.NetatmoApi = void 0;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ class NetatmoApi {
9
+ constructor(clientId, clientSecret, refreshToken, log, directToken) {
10
+ this.tokenExpiry = 0;
11
+ this.clientId = clientId;
12
+ this.clientSecret = clientSecret;
13
+ this.refreshToken = refreshToken;
14
+ this.log = log;
15
+ this.accessToken = directToken || '';
16
+ this.client = axios_1.default.create({
17
+ baseURL: 'https://api.netatmo.com',
18
+ timeout: 30000,
19
+ });
20
+ }
21
+ async authenticate() {
22
+ // If we have a direct token and it hasn't expired, use it
23
+ if (this.accessToken && Date.now() < this.tokenExpiry) {
24
+ return;
25
+ }
26
+ // If we have a direct token but no expiry set, assume valid for 3 hours
27
+ if (this.accessToken && this.tokenExpiry === 0) {
28
+ this.tokenExpiry = Date.now() + 10800000;
29
+ this.client.defaults.headers.common['Authorization'] = 'Bearer ' + this.accessToken;
30
+ this.log.info('Netatmo: using configured access token');
31
+ return;
32
+ }
33
+ // Use refresh_token to get a new access_token
34
+ if (!this.refreshToken) {
35
+ throw new Error('No refresh token configured. Please provide a refresh_token or access token.');
36
+ }
37
+ try {
38
+ this.log.info('Netatmo: refreshing access token...');
39
+ const res = await this.client.post('/oauth2/token', new URLSearchParams({
40
+ grant_type: 'refresh_token',
41
+ refresh_token: this.refreshToken,
42
+ client_id: this.clientId,
43
+ client_secret: this.clientSecret,
44
+ }).toString(), {
45
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
46
+ });
47
+ this.accessToken = res.data.access_token;
48
+ this.refreshToken = res.data.refresh_token || this.refreshToken;
49
+ this.tokenExpiry = Date.now() + (res.data.expires_in || 10800) * 1000;
50
+ this.client.defaults.headers.common['Authorization'] = 'Bearer ' + this.accessToken;
51
+ this.log.info('Netatmo: authenticated successfully (token valid for', Math.round((res.data.expires_in || 10800) / 3600), 'hours)');
52
+ }
53
+ catch (e) {
54
+ const msg = e instanceof Error ? e.message : String(e);
55
+ this.log.error('Netatmo: authentication failed -', msg);
56
+ throw e;
57
+ }
58
+ }
59
+ async getStationData() {
60
+ await this.authenticate();
61
+ try {
62
+ const res = await this.client.get('/api/getstationsdata', {
63
+ headers: { 'Authorization': 'Bearer ' + this.accessToken },
64
+ });
65
+ const devices = res.data?.body?.devices;
66
+ if (!devices || devices.length === 0) {
67
+ this.log.warn('Netatmo: no weather stations found');
68
+ return [];
69
+ }
70
+ const modules = [];
71
+ for (const device of devices) {
72
+ // Main indoor station
73
+ const mainData = device.dashboard_data || {};
74
+ modules.push({
75
+ id: device._id,
76
+ name: device.station_name || device.module_name || 'Indoor',
77
+ type: device.type || 'NAMain',
78
+ temperature: mainData.Temperature ?? null,
79
+ humidity: mainData.Humidity ?? null,
80
+ co2: mainData.CO2 ?? null,
81
+ pressure: mainData.Pressure ?? null,
82
+ noise: mainData.Noise ?? null,
83
+ battery: null, // main station is plugged in
84
+ reachable: true,
85
+ });
86
+ // Sub-modules (outdoor, extra indoor, rain, wind)
87
+ if (device.modules) {
88
+ for (const mod of device.modules) {
89
+ const modData = mod.dashboard_data || {};
90
+ // Only include modules that have temperature
91
+ if (modData.Temperature !== undefined) {
92
+ modules.push({
93
+ id: mod._id,
94
+ name: mod.module_name || mod.type || 'Module',
95
+ type: mod.type || 'unknown',
96
+ temperature: modData.Temperature ?? null,
97
+ humidity: modData.Humidity ?? null,
98
+ co2: modData.CO2 ?? null,
99
+ pressure: null,
100
+ noise: null,
101
+ battery: mod.battery_percent ?? null,
102
+ reachable: mod.reachable ?? false,
103
+ });
104
+ }
105
+ }
106
+ }
107
+ }
108
+ this.log.info('Netatmo: found', modules.length, 'temperature modules');
109
+ return modules;
110
+ }
111
+ catch (e) {
112
+ // If 403/401, try to re-authenticate
113
+ if (axios_1.default.isAxiosError(e) && (e.response?.status === 401 || e.response?.status === 403)) {
114
+ this.log.warn('Netatmo: token expired, re-authenticating...');
115
+ this.tokenExpiry = 0;
116
+ this.accessToken = '';
117
+ await this.authenticate();
118
+ return this.getStationData();
119
+ }
120
+ const msg = e instanceof Error ? e.message : String(e);
121
+ this.log.error('Netatmo: failed to get station data -', msg);
122
+ throw e;
123
+ }
124
+ }
125
+ }
126
+ exports.NetatmoApi = NetatmoApi;
@@ -0,0 +1,16 @@
1
+ export declare class NetatmoPlatform implements DynamicPlatformPlugin {
2
+ readonly log: Logger;
3
+ readonly config: PlatformConfig;
4
+ readonly api: API;
5
+ readonly Service: typeof Service;
6
+ readonly Characteristic: typeof Characteristic;
7
+ private netatmoApi;
8
+ private cachedAccessories;
9
+ private pollingInterval;
10
+ private modules;
11
+ constructor(log: Logger, config: PlatformConfig, api: API);
12
+ configureAccessory(accessory: PlatformAccessory): void;
13
+ onReady(): Promise<void>;
14
+ registerAccessories(modules: NetatmoModule[]): void;
15
+ poll(): Promise<void>;
16
+ }
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NetatmoPlatform = void 0;
4
+ class NetatmoPlatform {
5
+ constructor(log, config, api) {
6
+ this.log = log;
7
+ this.config = config;
8
+ this.api = api;
9
+ this.Service = this.api.hap.Service;
10
+ this.Characteristic = this.api.hap.Characteristic;
11
+ this.cachedAccessories = [];
12
+ this.modules = new Map();
13
+ this.pollingInterval = (config.pollingInterval || 300) * 1000; // default 5 min
14
+ this.api.on('didFinishLaunching', () => this.onReady());
15
+ }
16
+ configureAccessory(accessory) {
17
+ this.log.info('Restoring cached accessory:', accessory.displayName);
18
+ this.cachedAccessories.push(accessory);
19
+ }
20
+ async onReady() {
21
+ // Validate config
22
+ if (!this.config.refreshToken && !this.config.token) {
23
+ this.log.error('Missing refresh_token or token in config. Please configure Netatmo credentials.');
24
+ return;
25
+ }
26
+ this.netatmoApi = new NetatmoApi(this.config.clientId || '', this.config.clientSecret || '', this.config.refreshToken || '', this.log, this.config.token || '');
27
+ try {
28
+ await this.netatmoApi.authenticate();
29
+ }
30
+ catch {
31
+ this.log.error('Failed to connect to Netatmo API');
32
+ return;
33
+ }
34
+ // Get initial data and register accessories
35
+ try {
36
+ const modules = await this.netatmoApi.getStationData();
37
+ this.registerAccessories(modules);
38
+ this.log.info('Netatmo: total sensors registered:', modules.length);
39
+ }
40
+ catch (e) {
41
+ this.log.error('Failed to get initial Netatmo data');
42
+ return;
43
+ }
44
+ // Start polling
45
+ setInterval(() => this.poll(), this.pollingInterval);
46
+ }
47
+ registerAccessories(modules) {
48
+ const newAccessories = [];
49
+ for (const mod of modules) {
50
+ const uuid = this.api.hap.uuid.generate(PLUGIN_NAME + '-' + mod.id);
51
+ let accessory = this.cachedAccessories.find(a => a.UUID === uuid);
52
+ if (!accessory) {
53
+ const displayName = mod.name;
54
+ accessory = new this.api.platformAccessory(displayName, uuid);
55
+ newAccessories.push(accessory);
56
+ this.log.info('Adding new accessory:', displayName);
57
+ }
58
+ // Setup TemperatureSensor service
59
+ let tempService = accessory.getService(this.Service.TemperatureSensor);
60
+ if (!tempService) {
61
+ tempService = accessory.addService(this.Service.TemperatureSensor, mod.name);
62
+ }
63
+ // Set initial temperature
64
+ if (mod.temperature !== null) {
65
+ tempService.getCharacteristic(this.Characteristic.CurrentTemperature)
66
+ .updateValue(mod.temperature);
67
+ }
68
+ // Setup HumiditySensor service if humidity data available
69
+ if (mod.humidity !== null) {
70
+ let humService = accessory.getService(this.Service.HumiditySensor);
71
+ if (!humService) {
72
+ humService = accessory.addService(this.Service.HumiditySensor, mod.name + ' Humidity');
73
+ }
74
+ humService.getCharacteristic(this.Characteristic.CurrentRelativeHumidity)
75
+ .updateValue(mod.humidity);
76
+ }
77
+ // Set accessory info
78
+ const infoService = accessory.getService(this.Service.AccessoryInformation);
79
+ if (infoService) {
80
+ infoService
81
+ .setCharacteristic(this.Characteristic.Manufacturer, 'Netatmo')
82
+ .setCharacteristic(this.Characteristic.Model, mod.type)
83
+ .setCharacteristic(this.Characteristic.SerialNumber, mod.id);
84
+ }
85
+ this.modules.set(mod.id, { accessory, data: mod });
86
+ }
87
+ if (newAccessories.length > 0) {
88
+ this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, newAccessories);
89
+ }
90
+ // Remove orphaned accessories
91
+ const activeUUIDs = new Set(modules.map(m => this.api.hap.uuid.generate(PLUGIN_NAME + '-' + m.id)));
92
+ const orphans = this.cachedAccessories.filter(a => !activeUUIDs.has(a.UUID));
93
+ if (orphans.length > 0) {
94
+ this.log.info('Removing', orphans.length, 'orphaned accessories');
95
+ this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, orphans);
96
+ }
97
+ }
98
+ async poll() {
99
+ try {
100
+ const modules = await this.netatmoApi.getStationData();
101
+ for (const mod of modules) {
102
+ const entry = this.modules.get(mod.id);
103
+ if (!entry) {
104
+ continue;
105
+ }
106
+ const { accessory } = entry;
107
+ // Update temperature
108
+ if (mod.temperature !== null) {
109
+ const tempService = accessory.getService(this.Service.TemperatureSensor);
110
+ if (tempService) {
111
+ tempService.getCharacteristic(this.Characteristic.CurrentTemperature)
112
+ .updateValue(mod.temperature);
113
+ }
114
+ }
115
+ // Update humidity
116
+ if (mod.humidity !== null) {
117
+ const humService = accessory.getService(this.Service.HumiditySensor);
118
+ if (humService) {
119
+ humService.getCharacteristic(this.Characteristic.CurrentRelativeHumidity)
120
+ .updateValue(mod.humidity);
121
+ }
122
+ }
123
+ entry.data = mod;
124
+ }
125
+ // Log summary
126
+ const summary = modules.map(m => m.name + '=' + (m.temperature !== null ? m.temperature + '°C' : 'N/A') +
127
+ (m.humidity !== null ? ' ' + m.humidity + '%' : '')).join(', ');
128
+ this.log.info('Netatmo poll:', summary);
129
+ }
130
+ catch (e) {
131
+ const msg = e instanceof Error ? e.message : String(e);
132
+ this.log.error('Netatmo poll failed:', msg);
133
+ }
134
+ }
135
+ }
136
+ exports.NetatmoPlatform = NetatmoPlatform;
@@ -0,0 +1,2 @@
1
+ export declare const PLATFORM_NAME = "NetatmoTemperature";
2
+ export declare const PLUGIN_NAME = "homebridge-netatmo-temperature";
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PLUGIN_NAME = exports.PLATFORM_NAME = void 0;
4
+ exports.PLATFORM_NAME = 'NetatmoTemperature';
5
+ exports.PLUGIN_NAME = 'homebridge-netatmo-temperature';
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "homebridge-netatmo-temperature",
3
+ "version": "1.0.0",
4
+ "description": "Homebridge plugin for Netatmo weather station temperatures in Apple HomeKit",
5
+ "license": "Apache-2.0",
6
+ "author": "trama2000",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/trama2000/homebridge-netatmo-temperature.git"
10
+ },
11
+ "main": "dist/index.js",
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "prepublishOnly": "echo skip"
15
+ },
16
+ "keywords": [
17
+ "homebridge-plugin",
18
+ "netatmo",
19
+ "temperature",
20
+ "weather",
21
+ "homekit"
22
+ ],
23
+ "engines": {
24
+ "homebridge": ">=1.6.0",
25
+ "node": ">=18.0.0"
26
+ },
27
+ "dependencies": {
28
+ "axios": "^1.6.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^20.0.0",
32
+ "homebridge": "^1.8.0",
33
+ "typescript": "^5.0.0"
34
+ }
35
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export = (api: API) => {
2
+ api.registerPlatform(PLATFORM_NAME, NetatmoPlatform);
3
+ };
@@ -0,0 +1,156 @@
1
+ import axios, { AxiosInstance } from 'axios';
2
+ export interface NetatmoModule {
3
+ id: string;
4
+ name: string;
5
+ type: string; // NAMain, NAModule1 (outdoor), NAModule4 (indoor extra)
6
+ temperature: number | null;
7
+ humidity: number | null;
8
+ co2?: number | null;
9
+ pressure?: number | null;
10
+ noise?: number | null;
11
+ battery?: number | null;
12
+ reachable: boolean;
13
+ }
14
+
15
+ export class NetatmoApi {
16
+ private client: AxiosInstance;
17
+ private accessToken: string;
18
+ private refreshToken: string;
19
+ private clientId: string;
20
+ private clientSecret: string;
21
+ private tokenExpiry = 0;
22
+ private log: Logger;
23
+
24
+ constructor(
25
+ clientId: string,
26
+ clientSecret: string,
27
+ refreshToken: string,
28
+ log: Logger,
29
+ directToken?: string,
30
+ ) {
31
+ this.clientId = clientId;
32
+ this.clientSecret = clientSecret;
33
+ this.refreshToken = refreshToken;
34
+ this.log = log;
35
+ this.accessToken = directToken || '';
36
+
37
+ this.client = axios.create({
38
+ baseURL: 'https://api.netatmo.com',
39
+ timeout: 30000,
40
+ });
41
+ }
42
+
43
+ async authenticate(): Promise<void> {
44
+ // If we have a direct token and it hasn't expired, use it
45
+ if (this.accessToken && Date.now() < this.tokenExpiry) {
46
+ return;
47
+ }
48
+
49
+ // If we have a direct token but no expiry set, assume valid for 3 hours
50
+ if (this.accessToken && this.tokenExpiry === 0) {
51
+ this.tokenExpiry = Date.now() + 10800000;
52
+ this.client.defaults.headers.common['Authorization'] = 'Bearer ' + this.accessToken;
53
+ this.log.info('Netatmo: using configured access token');
54
+ return;
55
+ }
56
+
57
+ // Use refresh_token to get a new access_token
58
+ if (!this.refreshToken) {
59
+ throw new Error('No refresh token configured. Please provide a refresh_token or access token.');
60
+ }
61
+
62
+ try {
63
+ this.log.info('Netatmo: refreshing access token...');
64
+ const res = await this.client.post('/oauth2/token', new URLSearchParams({
65
+ grant_type: 'refresh_token',
66
+ refresh_token: this.refreshToken,
67
+ client_id: this.clientId,
68
+ client_secret: this.clientSecret,
69
+ }).toString(), {
70
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
71
+ });
72
+
73
+ this.accessToken = res.data.access_token;
74
+ this.refreshToken = res.data.refresh_token || this.refreshToken;
75
+ this.tokenExpiry = Date.now() + (res.data.expires_in || 10800) * 1000;
76
+ this.client.defaults.headers.common['Authorization'] = 'Bearer ' + this.accessToken;
77
+ this.log.info('Netatmo: authenticated successfully (token valid for', Math.round((res.data.expires_in || 10800) / 3600), 'hours)');
78
+ } catch (e) {
79
+ const msg = e instanceof Error ? e.message : String(e);
80
+ this.log.error('Netatmo: authentication failed -', msg);
81
+ throw e;
82
+ }
83
+ }
84
+
85
+ async getStationData(): Promise<NetatmoModule[]> {
86
+ await this.authenticate();
87
+
88
+ try {
89
+ const res = await this.client.get('/api/getstationsdata', {
90
+ headers: { 'Authorization': 'Bearer ' + this.accessToken },
91
+ });
92
+
93
+ const devices = res.data?.body?.devices;
94
+ if (!devices || devices.length === 0) {
95
+ this.log.warn('Netatmo: no weather stations found');
96
+ return [];
97
+ }
98
+
99
+ const modules: NetatmoModule[] = [];
100
+
101
+ for (const device of devices) {
102
+ // Main indoor station
103
+ const mainData = device.dashboard_data || {};
104
+ modules.push({
105
+ id: device._id,
106
+ name: device.station_name || device.module_name || 'Indoor',
107
+ type: device.type || 'NAMain',
108
+ temperature: mainData.Temperature ?? null,
109
+ humidity: mainData.Humidity ?? null,
110
+ co2: mainData.CO2 ?? null,
111
+ pressure: mainData.Pressure ?? null,
112
+ noise: mainData.Noise ?? null,
113
+ battery: null, // main station is plugged in
114
+ reachable: true,
115
+ });
116
+
117
+ // Sub-modules (outdoor, extra indoor, rain, wind)
118
+ if (device.modules) {
119
+ for (const mod of device.modules) {
120
+ const modData = mod.dashboard_data || {};
121
+ // Only include modules that have temperature
122
+ if (modData.Temperature !== undefined) {
123
+ modules.push({
124
+ id: mod._id,
125
+ name: mod.module_name || mod.type || 'Module',
126
+ type: mod.type || 'unknown',
127
+ temperature: modData.Temperature ?? null,
128
+ humidity: modData.Humidity ?? null,
129
+ co2: modData.CO2 ?? null,
130
+ pressure: null,
131
+ noise: null,
132
+ battery: mod.battery_percent ?? null,
133
+ reachable: mod.reachable ?? false,
134
+ });
135
+ }
136
+ }
137
+ }
138
+ }
139
+
140
+ this.log.info('Netatmo: found', modules.length, 'temperature modules');
141
+ return modules;
142
+ } catch (e: unknown) {
143
+ // If 403/401, try to re-authenticate
144
+ if (axios.isAxiosError(e) && (e.response?.status === 401 || e.response?.status === 403)) {
145
+ this.log.warn('Netatmo: token expired, re-authenticating...');
146
+ this.tokenExpiry = 0;
147
+ this.accessToken = '';
148
+ await this.authenticate();
149
+ return this.getStationData();
150
+ }
151
+ const msg = e instanceof Error ? e.message : String(e);
152
+ this.log.error('Netatmo: failed to get station data -', msg);
153
+ throw e;
154
+ }
155
+ }
156
+ }
@@ -0,0 +1,166 @@
1
+ export class NetatmoPlatform implements DynamicPlatformPlugin {
2
+ public readonly Service: typeof Service = this.api.hap.Service;
3
+ public readonly Characteristic: typeof Characteristic = this.api.hap.Characteristic;
4
+
5
+ private netatmoApi!: NetatmoApi;
6
+ private cachedAccessories: PlatformAccessory[] = [];
7
+ private pollingInterval: number;
8
+ private modules: Map<string, { accessory: PlatformAccessory; data: NetatmoModule }> = new Map();
9
+
10
+ constructor(
11
+ public readonly log: Logger,
12
+ public readonly config: PlatformConfig,
13
+ public readonly api: API,
14
+ ) {
15
+ this.pollingInterval = (config.pollingInterval || 300) * 1000; // default 5 min
16
+ this.api.on('didFinishLaunching', () => this.onReady());
17
+ }
18
+
19
+ configureAccessory(accessory: PlatformAccessory) {
20
+ this.log.info('Restoring cached accessory:', accessory.displayName);
21
+ this.cachedAccessories.push(accessory);
22
+ }
23
+
24
+ async onReady() {
25
+ // Validate config
26
+ if (!this.config.refreshToken && !this.config.token) {
27
+ this.log.error('Missing refresh_token or token in config. Please configure Netatmo credentials.');
28
+ return;
29
+ }
30
+
31
+ this.netatmoApi = new NetatmoApi(
32
+ this.config.clientId || '',
33
+ this.config.clientSecret || '',
34
+ this.config.refreshToken || '',
35
+ this.log,
36
+ this.config.token || '',
37
+ );
38
+
39
+ try {
40
+ await this.netatmoApi.authenticate();
41
+ } catch {
42
+ this.log.error('Failed to connect to Netatmo API');
43
+ return;
44
+ }
45
+
46
+ // Get initial data and register accessories
47
+ try {
48
+ const modules = await this.netatmoApi.getStationData();
49
+ this.registerAccessories(modules);
50
+ this.log.info('Netatmo: total sensors registered:', modules.length);
51
+ } catch (e) {
52
+ this.log.error('Failed to get initial Netatmo data');
53
+ return;
54
+ }
55
+
56
+ // Start polling
57
+ setInterval(() => this.poll(), this.pollingInterval);
58
+ }
59
+
60
+ registerAccessories(modules: NetatmoModule[]) {
61
+ const newAccessories: PlatformAccessory[] = [];
62
+
63
+ for (const mod of modules) {
64
+ const uuid = this.api.hap.uuid.generate(PLUGIN_NAME + '-' + mod.id);
65
+ let accessory = this.cachedAccessories.find(a => a.UUID === uuid);
66
+
67
+ if (!accessory) {
68
+ const displayName = mod.name;
69
+ accessory = new this.api.platformAccessory(displayName, uuid);
70
+ newAccessories.push(accessory);
71
+ this.log.info('Adding new accessory:', displayName);
72
+ }
73
+
74
+ // Setup TemperatureSensor service
75
+ let tempService = accessory.getService(this.Service.TemperatureSensor);
76
+ if (!tempService) {
77
+ tempService = accessory.addService(this.Service.TemperatureSensor, mod.name);
78
+ }
79
+
80
+ // Set initial temperature
81
+ if (mod.temperature !== null) {
82
+ tempService.getCharacteristic(this.Characteristic.CurrentTemperature)
83
+ .updateValue(mod.temperature);
84
+ }
85
+
86
+ // Setup HumiditySensor service if humidity data available
87
+ if (mod.humidity !== null) {
88
+ let humService = accessory.getService(this.Service.HumiditySensor);
89
+ if (!humService) {
90
+ humService = accessory.addService(this.Service.HumiditySensor, mod.name + ' Humidity');
91
+ }
92
+ humService.getCharacteristic(this.Characteristic.CurrentRelativeHumidity)
93
+ .updateValue(mod.humidity);
94
+ }
95
+
96
+ // Set accessory info
97
+ const infoService = accessory.getService(this.Service.AccessoryInformation);
98
+ if (infoService) {
99
+ infoService
100
+ .setCharacteristic(this.Characteristic.Manufacturer, 'Netatmo')
101
+ .setCharacteristic(this.Characteristic.Model, mod.type)
102
+ .setCharacteristic(this.Characteristic.SerialNumber, mod.id);
103
+ }
104
+
105
+ this.modules.set(mod.id, { accessory, data: mod });
106
+ }
107
+
108
+ if (newAccessories.length > 0) {
109
+ this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, newAccessories);
110
+ }
111
+
112
+ // Remove orphaned accessories
113
+ const activeUUIDs = new Set(modules.map(m => this.api.hap.uuid.generate(PLUGIN_NAME + '-' + m.id)));
114
+ const orphans = this.cachedAccessories.filter(a => !activeUUIDs.has(a.UUID));
115
+ if (orphans.length > 0) {
116
+ this.log.info('Removing', orphans.length, 'orphaned accessories');
117
+ this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, orphans);
118
+ }
119
+ }
120
+
121
+ async poll() {
122
+ try {
123
+ const modules = await this.netatmoApi.getStationData();
124
+
125
+ for (const mod of modules) {
126
+ const entry = this.modules.get(mod.id);
127
+ if (!entry) {
128
+ continue;
129
+ }
130
+
131
+ const { accessory } = entry;
132
+
133
+ // Update temperature
134
+ if (mod.temperature !== null) {
135
+ const tempService = accessory.getService(this.Service.TemperatureSensor);
136
+ if (tempService) {
137
+ tempService.getCharacteristic(this.Characteristic.CurrentTemperature)
138
+ .updateValue(mod.temperature);
139
+ }
140
+ }
141
+
142
+ // Update humidity
143
+ if (mod.humidity !== null) {
144
+ const humService = accessory.getService(this.Service.HumiditySensor);
145
+ if (humService) {
146
+ humService.getCharacteristic(this.Characteristic.CurrentRelativeHumidity)
147
+ .updateValue(mod.humidity);
148
+ }
149
+ }
150
+
151
+ entry.data = mod;
152
+ }
153
+
154
+ // Log summary
155
+ const summary = modules.map(m =>
156
+ m.name + '=' + (m.temperature !== null ? m.temperature + '°C' : 'N/A') +
157
+ (m.humidity !== null ? ' ' + m.humidity + '%' : '')
158
+ ).join(', ');
159
+ this.log.info('Netatmo poll:', summary);
160
+
161
+ } catch (e) {
162
+ const msg = e instanceof Error ? e.message : String(e);
163
+ this.log.error('Netatmo poll failed:', msg);
164
+ }
165
+ }
166
+ }
@@ -0,0 +1,2 @@
1
+ export const PLATFORM_NAME = 'NetatmoTemperature';
2
+ export const PLUGIN_NAME = 'homebridge-netatmo-temperature';
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": [
6
+ "ES2020"
7
+ ],
8
+ "declaration": true,
9
+ "strict": true,
10
+ "noImplicitAny": true,
11
+ "strictNullChecks": true,
12
+ "outDir": "./dist",
13
+ "rootDir": "./src",
14
+ "esModuleInterop": true,
15
+ "forceConsistentCasingInFileNames": true
16
+ },
17
+ "include": [
18
+ "src/"
19
+ ]
20
+ }