diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bebaa7..95e77f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to this project will be documented in this file. This project uses [Semantic Versioning](https://semver.org/). +## v4.0.0 +* Added Local UDP API support! Now you can choose to listen to your Weather Stations observations directly over your local network. No Station ID or API Token needed. Observations are broadcasted every 60 seconds. This leverages the `obs_st` message. See [documentation](https://weatherflow.github.io/Tempest/api/udp/v171/) for more information. + ## v3.0.3 * Update node-version: [18.x, 20.x], remove 16.x which is no longer supported by homebridge. * Reformated `getStationObservation()` and `getStationCurrentObservation()` in `tempestApi.ts`. diff --git a/README.md b/README.md index c052932..953a3e2 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ +*New* in v4.0.0 Local API Support! + Homebridge Plugin providing basic WeatherFlow Tempest support. Exposing 7 Acessories. - Temperature Sensor @@ -31,12 +33,13 @@ It is recommended when upgrading to v3.0 of the plugin from a prior version that ### Setup and Parameters -You will need to create an account at https://tempestwx.com/ and then generate a Personal Use Token https://tempestwx.com/settings/tokens. +Local API is now supported which requires no authentication. If you choose to use the non-local HTTP API you will need to create an account at https://tempestwx.com/ and then generate a Personal Use Token https://tempestwx.com/settings/tokens. - `name`: _(Required)_ Must always be set to `WeatherFlow Tempest Platform`. -- `token`: _(Required)_ Oauth2 Personal Use Token, create via your tempestwx account. -- `station_id`: _(Required)_ The station ID you are pulling weather data from. -- `interval`: _(Required)_ How often to poll the Tempest REST API. Default 10 seconds. Minimum every second. +- `local_api`: _(Required)_ Use the Local API versus HTTP API. +- `token`: _(Required for HTTP API)_ Oauth2 Personal Use Token, create via your tempestwx account. +- `station_id`: _(Required for HTTP API)_ The station ID you are pulling weather data from. +- `interval`: _(Required for HTTP API)_ How often to poll the Tempest REST API. Default 10 seconds. Minimum every second. - `sensors`: _(Required)_ An array of sensors to create. This is dynamic incase you want to target different temperature or wind speed attributes. - `sensors[].name`: _(Required)_ Display name of Sensor in Apple Home. - `sensors[].sensor_type`: _(Required)_ The type of Home Sensor to create. There are 6 options ["Temperature Sensor", "Light Sensor", "Humidity Sensor", "Fan", "Motion Sensor", "Occupancy Sensor"]. @@ -58,7 +61,7 @@ sensor_type `{2}` | value_key | metric units | std units | additional_properties `Motion Sensor` | wind_gust | m/s | mi/hr | motion_trigger_value | 5 | 10 | `Occupancy Sensor {3}{4}` | barometric_pressure | mb | inHg | occupancy_trigger_value | 1000 | 30 | ` ` | precip | mm/min | in/hr | occupancy_trigger_value | 0.1 | 0.25 | -` ` | precip_accum_local_day | mm | in | occupancy_trigger_value | 25 | 1 | +` ` | precip_accum_local_day | mm | in | occupancy_trigger_value | 25 | 1 | **Not available with Local API** ` ` | solar_radiation | W/m^2 | W/m^2 | occupancy_trigger_value | 1000| 1000 | ` ` | uv | Index | Index | occupancy_trigger_value | 3 | 3 | ` ` | wind_direction | degrees | degrees | occupancy_trigger_value | 360 | 360 | @@ -69,11 +72,109 @@ sensor_type `{2}` | value_key | metric units | std units | additional_properties `{4}` NOTE: There is a current limitation with v3.0.0 of the plug-in in that HomeKit accessory names are set when the accessory is initially added and cannot be dynamically updated. The accessories are correctly displayed and updated in the Homebridge "Accessories" tab of the webpage interface. Occupancy sensors `trigger_value` status is correctly displayed in both HomeKit and Homebridge. -### Config Example +### Local API Config Example + +```json +{ + "name": "WeatherFlow Tempest Platform", + "local_api": true, + "units": "Standard", + "sensors": [ + { + "name": "Temperature", + "sensor_type": "Temperature Sensor", + "temperature_properties": { + "value_key": "air_temperature" + } + }, + { + "name": "Relative Humidity", + "sensor_type": "Humidity Sensor", + "humidity_properties": { + "value_key": "relative_humidity" + } + }, + { + "name": "Light Level", + "sensor_type": "Light Sensor", + "light_properties": { + "value_key": "brightness" + } + }, + { + "name": "Wind Speed", + "sensor_type": "Fan", + "fan_properties": { + "value_key": "wind_avg" + } + }, + { + "name": "Wind Gust", + "sensor_type": "Motion Sensor", + "motion_properties": { + "value_key": "wind_gust", + "trigger_value": 10 + } + }, + { + "name": "Barometer", + "sensor_type": "Occupancy Sensor", + "occupancy_properties": { + "value_key": "barometric_pressure", + "trigger_value": 30 + } + }, + { + "name": "Solar Radiation", + "sensor_type": "Occupancy Sensor", + "occupancy_properties": { + "value_key": "solar_radiation", + "trigger_value": 1000 + } + }, + { + "name": "UV", + "sensor_type": "Occupancy Sensor", + "occupancy_properties": { + "value_key": "uv", + "trigger_value": 3 + } + }, + { + "name": "Precipitation Rate", + "sensor_type": "Occupancy Sensor", + "occupancy_properties": { + "value_key": "precip", + "trigger_value": 0.25 + } + }, + { + "name": "Precipitation Today", + "sensor_type": "Occupancy Sensor", + "occupancy_properties": { + "value_key": "precip_accum_local_day", + "trigger_value": 1 + } + }, + { + "name": "Wind Direction", + "sensor_type": "Occupancy Sensor", + "occupancy_properties": { + "value_key": "wind_direction", + "trigger_value": 360 + } + } + ], + "platform": "WeatherFlowTempest" +} +``` + +### HTTP API Config Example ```json { "name": "WeatherFlow Tempest Platform", + "local_api": false, "token": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", "station_id": 10000, "interval": 10, diff --git a/config.schema.json b/config.schema.json index ff4810f..58171c5 100644 --- a/config.schema.json +++ b/config.schema.json @@ -1,182 +1,210 @@ { - "pluginAlias": "WeatherFlowTempest", - "pluginType": "platform", - "singular": false, - "schema": { - "type": "object", - "properties": { - "name": { - "title": "Name", - "type": "string", - "required": true, - "default": "WeatherFlow Tempest Platform" - }, - "token": { - "title": "Token", - "type": "string", - "required": true - }, - "station_id": { - "title": "Station ID (Integer)", - "type": "number", - "required": true - }, - "interval": { - "title": "Interval (seconds)", - "type": "integer", - "default": 10, - "minimum": 1 - }, - "units": { - "title": "Units", - "type": "string", - "enum": [ - "Standard", - "Metric" - ], - "default": "Standard" - }, - "sensors": { - "title": "Weather Sensors", - "description": "Enable WeatherFlow Tempest Sensors.", - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "title": "Name", - "type": "string", - "required": true - }, - "sensor_type": { - "type": "string", - "enum": [ - "Temperature Sensor", - "Light Sensor", - "Humidity Sensor", - "Fan", - "Motion Sensor", - "Occupancy Sensor" - ], - "default": "Temperature Sensor" - }, - "fan_properties": { - "title": "Fan Properties", - "type": "object", - "condition": { - "functionBody": "if (model.sensors[arrayIndices] && model.sensors[arrayIndices].sensor_type && model.sensors[arrayIndices].sensor_type === 'Fan') { return true; } else { return false; };" - }, - "properties": { - "value_key": { - "type": "string", - "enum": [ - "wind_avg" - ] - } - } - }, - "light_properties": { - "title": "Light Properties", - "type": "object", - "condition": { - "functionBody": "if (model.sensors[arrayIndices] && model.sensors[arrayIndices].sensor_type && model.sensors[arrayIndices].sensor_type === 'Light Sensor') { return true; } else { return false; };" - }, - "properties": { - "value_key": { - "type": "string", - "enum": [ - "brightness" - ] - } - } - }, - "humidity_properties": { - "title": "Humidity Properties", - "type": "object", - "condition": { - "functionBody": "if (model.sensors[arrayIndices] && model.sensors[arrayIndices].sensor_type && model.sensors[arrayIndices].sensor_type === 'Humidity Sensor') { return true; } else { return false; };" - }, - "properties": { - "value_key": { - "type": "string", - "enum": [ - "relative_humidity" - ] - } - } - }, - "temperature_properties": { - "title": "Temperature Properties", - "type": "object", - "condition": { - "functionBody": "if (model.sensors[arrayIndices] && model.sensors[arrayIndices].sensor_type && model.sensors[arrayIndices].sensor_type === 'Temperature Sensor') { return true; } else { return false; };" - }, - "properties": { - "value_key": { - "type": "string", - "enum": [ - "air_temperature", - "dew_point", - "feels_like", - "wind_chill" - ] - } - } - }, - "motion_properties": { - "title": "Motion Properties", - "type": "object", - "condition": { - "functionBody": "if (model.sensors[arrayIndices] && model.sensors[arrayIndices].sensor_type && model.sensors[arrayIndices].sensor_type === 'Motion Sensor') { return true; } else { return false; };" - }, - "properties": { - "value_key": { - "type": "string", - "enum": [ - "wind_gust" - ] + "pluginAlias": "WeatherFlowTempest", + "pluginType": "platform", + "singular": false, + "schema": { + "type": "object", + + "properties": { + "name": { + "title": "Name", + "type": "string", + "required": true, + "default": "WeatherFlow Tempest Platform" + }, + "local_api": { + "title": "Use Local API", + "type": "boolean", + "required": true, + "default": true + }, + "token": { + "title": "Token", + "type": "string", + "default": "", + "condition": { + "functionBody": "if (model.local_api != undefined && !model.local_api) { return true; } else { return false; };" + } + }, + "station_id": { + "title": "Station ID (Integer)", + "type": "number", + "default": 0, + "condition": { + "functionBody": "if (model.local_api != undefined && !model.local_api) { return true; } else { return false; };" + } + }, + "interval": { + "title": "Interval (seconds)", + "type": "integer", + "default": 10, + "minimum": 1, + "condition": { + "functionBody": "if (model.local_api != undefined && !model.local_api) { return true; } else { return false; };" + } + }, + "units": { + "title": "Units", + "type": "string", + "enum": [ + "Standard", + "Metric" + ], + "default": "Standard" + }, + "sensors": { + "title": "Weather Sensors", + "description": "Enable WeatherFlow Tempest Sensors.", + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "title": "Name", + "type": "string", + "required": true + }, + "sensor_type": { + "type": "string", + "enum": [ + "Temperature Sensor", + "Light Sensor", + "Humidity Sensor", + "Fan", + "Motion Sensor", + "Occupancy Sensor" + ], + "default": "Temperature Sensor" + }, + "fan_properties": { + "title": "Fan Properties", + "type": "object", + "condition": { + "functionBody": "if (model.sensors[arrayIndices] && model.sensors[arrayIndices].sensor_type && model.sensors[arrayIndices].sensor_type === 'Fan') { return true; } else { return false; };" + }, + "properties": { + "value_key": { + "type": "string", + "enum": [ + "wind_avg" + ] + } + } + }, + "light_properties": { + "title": "Light Properties", + "type": "object", + "condition": { + "functionBody": "if (model.sensors[arrayIndices] && model.sensors[arrayIndices].sensor_type && model.sensors[arrayIndices].sensor_type === 'Light Sensor') { return true; } else { return false; };" + + }, + "properties": { + "value_key": { + "type": "string", + "enum": [ + "brightness" + ] + } + } + }, + "humidity_properties": { + "title": "Humidity Properties", + "type": "object", + "condition": { + "functionBody": "if (model.sensors[arrayIndices] && model.sensors[arrayIndices].sensor_type && model.sensors[arrayIndices].sensor_type === 'Humidity Sensor') { return true; } else { return false; };" + }, + "properties": { + "value_key": { + "type": "string", + "enum": [ + "relative_humidity" + ] + } + } + }, + "temperature_properties": { + "title": "Temperature Properties", + "type": "object", + "condition": { + "functionBody": "if (model.sensors[arrayIndices] && model.sensors[arrayIndices].sensor_type && model.sensors[arrayIndices].sensor_type === 'Temperature Sensor') { return true; } else { return false; };" + }, + "properties": { + "value_key": { + "type": "string", + "enum": [ + "air_temperature", + "dew_point", + "feels_like", + "wind_chill" + ] + } + } + }, + "motion_properties": { + "title": "Motion Properties", + "type": "object", + "condition": { + "functionBody": "if (model.sensors[arrayIndices] && model.sensors[arrayIndices].sensor_type && model.sensors[arrayIndices].sensor_type === 'Motion Sensor') { return true; } else { return false; };" + }, + "properties": { + "value_key": { + "type": "string", + "enum": [ + "wind_gust" + ] + }, + "trigger_value": { + "type": "number", + "minimum": 1, + "description": "At what point (value) to trigger motion detected on/off (1 minimum)." + } + } + }, + "occupancy_properties": { + "title": "Occupancy Properties", + "type": "object", + "condition": { + "functionBody": "if (model.sensors[arrayIndices] && model.sensors[arrayIndices].sensor_type && model.sensors[arrayIndices].sensor_type === 'Occupancy Sensor') { return true; } else { return false; };" + }, + "properties": { + "value_key": { + "type": "string", + "enum": [ + "barometric_pressure", + "precip", + "precip_accum_local_day", + "wind_direction", + "wind_gust", + "solar_radiation", + "uv" + ], + "description": "Note: `precip_accum_local_day` not supported when using Local API." }, - "trigger_value": { - "type": "number", - "minimum": 1, - "description": "At what point (value) to trigger motion detected on/off (1 minimum)." - } - } - }, - "occupancy_properties": { - "title": "Occupancy Properties", - "type": "object", - "condition": { - "functionBody": "if (model.sensors[arrayIndices] && model.sensors[arrayIndices].sensor_type && model.sensors[arrayIndices].sensor_type === 'Occupancy Sensor') { return true; } else { return false; };" - }, - "properties": { - "value_key": { - "type": "string", - "enum": [ - "barometric_pressure", - "precip", - "precip_accum_local_day", - "wind_direction", - "wind_gust", - "solar_radiation", - "uv" - ] - }, - "trigger_value": { - "type": "number", - "minimum": 0, - "description": "At what point (value) to trigger occupancy detected on/off (0 minimum)." - } - } - } - } - }, - "required": [ - "name", - "sensor_type", - "value_key" - ] - } - } - } + "trigger_value": { + "type": "number", + "minimum": 0, + "description": "At what point (value) to trigger occupancy detected on/off (0 minimum)." + } + } + } + } + }, + "required": [ + "name", + "sensor_type", + "value_key" + ] + } + }, + "required": ["local_api"], + "dependencies": { + "local_api": { + "not": { + "type": "boolean", + "const": true + }, + "required": ["token", "station_id"] + } + } + } } diff --git a/package-lock.json b/package-lock.json index ac6a6d7..9c1fdbe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "homebridge-weatherflow-tempest", - "version": "3.0.3", + "version": "4.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "homebridge-weatherflow-tempest", - "version": "3.0.3", + "version": "4.0.0", "license": "Apache-2.0", "dependencies": { "axios": "1.5.1" @@ -70,18 +70,18 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.9.1.tgz", - "integrity": "sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", - "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", + "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -102,9 +102,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.52.0.tgz", - "integrity": "sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==", + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz", + "integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -339,37 +339,37 @@ "dev": true }, "node_modules/@types/json-schema": { - "version": "7.0.14", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz", - "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==", + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, "node_modules/@types/node": { - "version": "20.8.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.7.tgz", - "integrity": "sha512-21TKHHh3eUHIi2MloeptJWALuCu5H7HQTdTrWIFReA8ad+aggoX+lRes3ex7/FtpC+sVUpFMQ+QTfYr74mruiQ==", + "version": "20.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz", + "integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==", "dev": true, "dependencies": { - "undici-types": "~5.25.1" + "undici-types": "~5.26.4" } }, "node_modules/@types/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==", + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.5.tgz", + "integrity": "sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg==", "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.8.0.tgz", - "integrity": "sha512-GosF4238Tkes2SHPQ1i8f6rMtG6zlKwMEB0abqSJ3Npvos+doIlc/ATG+vX1G9coDF3Ex78zM3heXHLyWEwLUw==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.10.0.tgz", + "integrity": "sha512-uoLj4g2OTL8rfUQVx2AFO1hp/zja1wABJq77P6IclQs6I/m9GLrm7jCdgzZkvWdDCQf1uEvoa8s8CupsgWQgVg==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.8.0", - "@typescript-eslint/type-utils": "6.8.0", - "@typescript-eslint/utils": "6.8.0", - "@typescript-eslint/visitor-keys": "6.8.0", + "@typescript-eslint/scope-manager": "6.10.0", + "@typescript-eslint/type-utils": "6.10.0", + "@typescript-eslint/utils": "6.10.0", + "@typescript-eslint/visitor-keys": "6.10.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -395,15 +395,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.8.0.tgz", - "integrity": "sha512-5tNs6Bw0j6BdWuP8Fx+VH4G9fEPDxnVI7yH1IAPkQH5RUtvKwRoqdecAPdQXv4rSOADAaz1LFBZvZG7VbXivSg==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.10.0.tgz", + "integrity": "sha512-+sZwIj+s+io9ozSxIWbNB5873OSdfeBEH/FR0re14WLI6BaKuSOnnwCJ2foUiu8uXf4dRp1UqHP0vrZ1zXGrog==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.8.0", - "@typescript-eslint/types": "6.8.0", - "@typescript-eslint/typescript-estree": "6.8.0", - "@typescript-eslint/visitor-keys": "6.8.0", + "@typescript-eslint/scope-manager": "6.10.0", + "@typescript-eslint/types": "6.10.0", + "@typescript-eslint/typescript-estree": "6.10.0", + "@typescript-eslint/visitor-keys": "6.10.0", "debug": "^4.3.4" }, "engines": { @@ -423,13 +423,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.8.0.tgz", - "integrity": "sha512-xe0HNBVwCph7rak+ZHcFD6A+q50SMsFwcmfdjs9Kz4qDh5hWhaPhFjRs/SODEhroBI5Ruyvyz9LfwUJ624O40g==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.10.0.tgz", + "integrity": "sha512-TN/plV7dzqqC2iPNf1KrxozDgZs53Gfgg5ZHyw8erd6jd5Ta/JIEcdCheXFt9b1NYb93a1wmIIVW/2gLkombDg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.8.0", - "@typescript-eslint/visitor-keys": "6.8.0" + "@typescript-eslint/types": "6.10.0", + "@typescript-eslint/visitor-keys": "6.10.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -440,13 +440,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.8.0.tgz", - "integrity": "sha512-RYOJdlkTJIXW7GSldUIHqc/Hkto8E+fZN96dMIFhuTJcQwdRoGN2rEWA8U6oXbLo0qufH7NPElUb+MceHtz54g==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.10.0.tgz", + "integrity": "sha512-wYpPs3hgTFblMYwbYWPT3eZtaDOjbLyIYuqpwuLBBqhLiuvJ+9sEp2gNRJEtR5N/c9G1uTtQQL5AhV0fEPJYcg==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.8.0", - "@typescript-eslint/utils": "6.8.0", + "@typescript-eslint/typescript-estree": "6.10.0", + "@typescript-eslint/utils": "6.10.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -467,9 +467,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.8.0.tgz", - "integrity": "sha512-p5qOxSum7W3k+llc7owEStXlGmSl8FcGvhYt8Vjy7FqEnmkCVlM3P57XQEGj58oqaBWDQXbJDZxwUWMS/EAPNQ==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.10.0.tgz", + "integrity": "sha512-36Fq1PWh9dusgo3vH7qmQAj5/AZqARky1Wi6WpINxB6SkQdY5vQoT2/7rW7uBIsPDcvvGCLi4r10p0OJ7ITAeg==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -480,13 +480,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.8.0.tgz", - "integrity": "sha512-ISgV0lQ8XgW+mvv5My/+iTUdRmGspducmQcDw5JxznasXNnZn3SKNrTRuMsEXv+V/O+Lw9AGcQCfVaOPCAk/Zg==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.10.0.tgz", + "integrity": "sha512-ek0Eyuy6P15LJVeghbWhSrBCj/vJpPXXR+EpaRZqou7achUWL8IdYnMSC5WHAeTWswYQuP2hAZgij/bC9fanBg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.8.0", - "@typescript-eslint/visitor-keys": "6.8.0", + "@typescript-eslint/types": "6.10.0", + "@typescript-eslint/visitor-keys": "6.10.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -507,17 +507,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.8.0.tgz", - "integrity": "sha512-dKs1itdE2qFG4jr0dlYLQVppqTE+Itt7GmIf/vX6CSvsW+3ov8PbWauVKyyfNngokhIO9sKZeRGCUo1+N7U98Q==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.10.0.tgz", + "integrity": "sha512-v+pJ1/RcVyRc0o4wAGux9x42RHmAjIGzPRo538Z8M1tVx6HOnoQBCX/NoadHQlZeC+QO2yr4nNSFWOoraZCAyg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.8.0", - "@typescript-eslint/types": "6.8.0", - "@typescript-eslint/typescript-estree": "6.8.0", + "@typescript-eslint/scope-manager": "6.10.0", + "@typescript-eslint/types": "6.10.0", + "@typescript-eslint/typescript-estree": "6.10.0", "semver": "^7.5.4" }, "engines": { @@ -532,12 +532,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.8.0.tgz", - "integrity": "sha512-oqAnbA7c+pgOhW2OhGvxm0t1BULX5peQI/rLsNDpGM78EebV3C9IGbX5HNZabuZ6UQrYveCLjKo8Iy/lLlBkkg==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.10.0.tgz", + "integrity": "sha512-xMGluxQIEtOM7bqFCo+rCMh5fqI+ZxV5RUUOa29iVPz1OgCZrtc7rFnz5cLUazlkPKYqX+75iuDq7m0HQ48nCg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.8.0", + "@typescript-eslint/types": "6.10.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -561,9 +561,9 @@ "dev": true }, "node_modules/acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -582,9 +582,9 @@ } }, "node_modules/acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz", + "integrity": "sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==", "dev": true, "engines": { "node": ">=0.4.0" @@ -874,12 +874,12 @@ } }, "node_modules/commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "dev": true, "engines": { - "node": ">= 6" + "node": ">= 10" } }, "node_modules/concat-map": { @@ -926,15 +926,15 @@ } }, "node_modules/deep-equal": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.2.tgz", - "integrity": "sha512-xjVyBf0w5vH0I42jdAZzOKVldmPgSulmiyPRywoyq7HXC9qdgo17kxJE+rdnif5Tz6+pIrpJI8dCpMNLIGkUiA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", "dev": true, "dependencies": { "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", + "call-bind": "^1.0.5", "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.1", + "get-intrinsic": "^1.2.2", "is-arguments": "^1.1.1", "is-array-buffer": "^3.0.2", "is-date-object": "^1.0.5", @@ -944,11 +944,14 @@ "object-is": "^1.1.5", "object-keys": "^1.1.1", "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.0", + "regexp.prototype.flags": "^1.5.1", "side-channel": "^1.0.4", "which-boxed-primitive": "^1.0.2", "which-collection": "^1.0.1", - "which-typed-array": "^1.1.9" + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1095,15 +1098,15 @@ } }, "node_modules/eslint": { - "version": "8.52.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.52.0.tgz", - "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==", + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz", + "integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.52.0", + "@eslint/eslintrc": "^2.1.3", + "@eslint/js": "8.53.0", "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -1258,9 +1261,9 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -1778,24 +1781,24 @@ } }, "node_modules/homebridge": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/homebridge/-/homebridge-1.6.1.tgz", - "integrity": "sha512-hDhSaBDHFbB8wQQuZKbistYj1gjTIcNWmusqgEUb0Umk76Hs+G6VKRTkOEEVuxRaQWoK5hRM5rJTsCGAMCj5cA==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/homebridge/-/homebridge-1.7.0.tgz", + "integrity": "sha512-2QikXnmpnFe2s33Q8TeYE5+sXyKHUZ+9l5WfDmpuupHdct6H/G6b6z3HCj+2rlMRKKY5ElLv5XtLoxOcafnL0g==", "dev": true, "dependencies": { "chalk": "^4.1.2", - "commander": "5.1.0", + "commander": "^7.2.0", "fs-extra": "^10.1.0", "hap-nodejs": "~0.11.1", "qrcode-terminal": "^0.12.0", - "semver": "^7.3.7", + "semver": "^7.5.4", "source-map-support": "^0.5.21" }, "bin": { "homebridge": "bin/homebridge" }, "engines": { - "node": ">=10.17.0" + "node": "^18.15.0 || ^20.7.0" } }, "node_modules/ignore": { @@ -2263,10 +2266,13 @@ "dev": true }, "node_modules/lru-cache": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", - "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.2.tgz", + "integrity": "sha512-Yj9mA8fPiVgOUpByoTZO5pNrcl5Yk37FcSHsUINpAsaBIEZIuqcCclDZJCVxqQShDsmYX8QG63svJiTbOATZwg==", "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, "engines": { "node": "14 || >=16.14" } @@ -2703,9 +2709,9 @@ "dev": true }, "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "engines": { "node": ">=6" @@ -3309,15 +3315,15 @@ "dev": true }, "node_modules/undici-types": { - "version": "5.25.3", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", - "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==", + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "dev": true }, "node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "engines": { "node": ">= 10.0.0" diff --git a/package.json b/package.json index 134732c..37665ee 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "private": false, "displayName": "Homebridge WeatherFlow Tempest", "name": "homebridge-weatherflow-tempest", - "version": "3.0.3", + "version": "4.0.0", "description": "Exposes WeatherFlow Tempest Station data as Temperature Sensors, Light Sensors, Humidity Sensors and Fan Sensors (for Wind Speed).", "license": "Apache-2.0", "repository": { diff --git a/src/index.ts b/src/index.ts index f493443..68605a7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,11 @@ import { API } from 'homebridge'; -import { PLATFORM_NAME } from './settings'; +import { PLATFORM_NAME, PLUGIN_NAME } from './settings'; import { WeatherFlowTempestPlatform } from './platform'; /** * This method registers the platform with Homebridge */ export = (api: API) => { - api.registerPlatform(PLATFORM_NAME, WeatherFlowTempestPlatform); + api.registerPlatform(PLUGIN_NAME, PLATFORM_NAME, WeatherFlowTempestPlatform); }; diff --git a/src/platform.ts b/src/platform.ts index 5880614..37f267e 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -1,9 +1,9 @@ -import { API, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig, Service, Characteristic } from 'homebridge'; +import { API, APIEvent, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig, Service, Characteristic } from 'homebridge'; import { PLATFORM_NAME, PLUGIN_NAME } from './settings'; import { WeatherFlowTempestPlatformAccessory } from './platformAccessory'; -import { TempestApi, Observation } from './tempestApi'; +import { TempestApi, TempestSocket, Observation } from './tempest'; interface TempestSensor { name: string; @@ -24,7 +24,8 @@ export class WeatherFlowTempestPlatform implements DynamicPlatformPlugin { public readonly Characteristic: typeof Characteristic = this.api.hap.Characteristic; public readonly accessories: PlatformAccessory[] = []; - private tempestApi: TempestApi; + private tempestApi: TempestApi | undefined; + private tempestSocket: TempestSocket | undefined; public observation_data: Observation; // Observation data for Accessories to use. public tempest_battery_level!: number; // Tempest battery level @@ -40,10 +41,7 @@ export class WeatherFlowTempestPlatform implements DynamicPlatformPlugin { log.info('Finished initializing platform:', this.config.name); - // Initialize TempestApi - this.tempestApi = new TempestApi(this.config.token, this.config.station_id, log); - - // initialize observation_data + // Initialize observation_data this.observation_data = { air_temperature: 0, barometric_pressure: 0, @@ -64,7 +62,7 @@ export class WeatherFlowTempestPlatform implements DynamicPlatformPlugin { this.tempest_device_id = 0; // Make sure the Station ID is the integer ID - if (isNaN(this.config.station_id)) { + if (this.config.local_api === false && isNaN(this.config.station_id)) { log.warn( 'Station ID is not an Integer! Please make sure you are using the ID integer found here: ' + 'https://tempestwx.com/station//', @@ -72,7 +70,7 @@ export class WeatherFlowTempestPlatform implements DynamicPlatformPlugin { return; } - this.api.on('didFinishLaunching', () => { + this.api.on(APIEvent.DID_FINISH_LAUNCHING, () => { log.info('Executed didFinishLaunching callback'); @@ -81,56 +79,133 @@ export class WeatherFlowTempestPlatform implements DynamicPlatformPlugin { return; } - try { + // Initialize Tempest Interfaces + if (this.config.local_api === true) { + this.initializeBySocket(); + } else { + this.initializeByApi(); + } - this.tempestApi.getStationCurrentObservation(0).then( (observation_data: Observation) => { + }); - if (!observation_data) { - log.warn('Failed to fetch initial Station Current Observations after retrying. Refusing to continue.'); - return; - } + } - // Cache the observation results - this.observation_data = observation_data; + private async initializeBySocket() { - // Initialize sensors after first API response. - this.discoverDevices(); + this.log.info('Initializing by Socket'); - this.log.debug ('discoverDevices completed'); + try { + this.log.info('Using Tempest Local API.'); + this.tempestSocket = new TempestSocket(this.log); + this.tempestSocket.start(); - // Remove cached sensors that are no longer required. - this.removeDevices(); + // Hold thread for first message and set values + await this.socketDataRecieved(); + this.observation_data = this.tempestSocket.getStationCurrentObservation(); + this.tempest_battery_level = this.tempestSocket.getBatteryLevel(); - this.log.debug ('removeDevices completed'); + // Initialize sensors after first API response. + this.discoverDevices(); + this.log.info ('discoverDevices completed'); - // Determine Tempest device_id & initial battery level - this.tempestApi.getTempestDeviceId().then( (device_id: number) => { - this.tempest_device_id = device_id; + // Remove cached sensors that are no longer required. + this.removeDevices(); + this.log.info ('removeDevices completed'); - this.tempestApi.getTempestBatteryLevel(this.tempest_device_id).then( (battery_level: number) => { + // Poll every minute for local API + this.pollLocalStationCurrentObservation(); - if (battery_level === undefined) { - this.log.warn('Failed to fetch initial Tempest battery level'); - return; - } - this.tempest_battery_level = battery_level; - }); + } catch(exception) { + this.log.error(exception as string); + } + } - }); + private socketDataRecieved(): Promise { + + this.log.info('Waiting for first local broadcast. This could take up to 60 seconds...'); + return new Promise((resolve) => { + const socket_interval = setInterval(() => { + if (this.tempestSocket === undefined) { + return; + } + if (this.tempestSocket.hasData()) { + clearInterval(socket_interval); + this.log.info('Initial local broadcast recieved.'); + resolve(); + } + }, 1000); + }); + + } + + private initializeByApi() { + + this.log.info('Initializing by API'); + + try { + this.log.info('Using Tempest RESTful API.'); + this.tempestApi = new TempestApi(this.config.token, this.config.station_id, this.log); + this.tempestApi.getStationCurrentObservation(0).then( (observation_data: Observation) => { + + if (!observation_data) { + this.log.warn('Failed to fetch initial Station Current Observations after retrying. Refusing to continue.'); + return; + } - // Then begin to poll the station current observations data. - this.pollStationCurrentObservation(); + if (this.tempestApi === undefined) { + return; + } + + // Cache the observation results + this.observation_data = observation_data; + + // Initialize sensors after first API response. + this.discoverDevices(); + this.log.info ('discoverDevices completed'); + + // Remove cached sensors that are no longer required. + this.removeDevices(); + this.log.info ('removeDevices completed'); + // Determine Tempest device_id & initial battery level + this.tempestApi.getTempestDeviceId().then( (device_id: number) => { + this.tempest_device_id = device_id; + if (this.tempestApi === undefined) { + return; + } + this.tempestApi.getTempestBatteryLevel(this.tempest_device_id).then( (battery_level: number) => { + if (battery_level === undefined) { + this.log.warn('Failed to fetch initial Tempest battery level'); + return; + } + this.tempest_battery_level = battery_level; + }); }); - } catch(exception) { + // Then begin to poll the station current observations data. + this.pollStationCurrentObservation(); + }); + + } catch(exception) { + this.log.error(exception as string); + } - this.log.error(exception as string); + } + private pollLocalStationCurrentObservation(): void { + + setInterval( async () => { + + if (this.tempestSocket === undefined) { + return; } - }); + // Update values + this.observation_data = this.tempestSocket.getStationCurrentObservation(); + this.tempest_battery_level = this.tempestSocket.getBatteryLevel(); + + }, 60 * 1000); // Tempest local API broadcasts every minute. } @@ -142,6 +217,10 @@ export class WeatherFlowTempestPlatform implements DynamicPlatformPlugin { setInterval( async () => { + if (this.tempestApi === undefined) { + return; + } + // Update Observation data await this.tempestApi.getStationCurrentObservation(0).then( (observation_data: Observation) => { @@ -238,6 +317,9 @@ export class WeatherFlowTempestPlatform implements DynamicPlatformPlugin { break; case 'Occupancy Sensor': value_key = device.occupancy_properties['value_key']; + if((this.config.local_api === true) && (value_key === 'precip_accum_local_day')) { + value_key = 'not_available'; + } break; default: this.log.warn('device.sensor_type not defined'); @@ -247,36 +329,39 @@ export class WeatherFlowTempestPlatform implements DynamicPlatformPlugin { `${device.name}-${device.sensor_type}-${value_key}`, ); - const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid); + if (value_key !== 'not_available') { + + const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid); - if (existingAccessory) { - this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName); + if (existingAccessory) { + this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName); - // pick up any changes such as 'trigger_value' - existingAccessory.context.device = device; + // pick up any changes such as 'trigger_value' + existingAccessory.context.device = device; - // update accessory context information - this.api.updatePlatformAccessories(this.accessories); + // update accessory context information + this.api.updatePlatformAccessories(this.accessories); - new WeatherFlowTempestPlatformAccessory(this, existingAccessory); + new WeatherFlowTempestPlatformAccessory(this, existingAccessory); - // add to array of active accessories - this.activeAccessory.push(existingAccessory); + // add to array of active accessories + this.activeAccessory.push(existingAccessory); - } else { - this.log.info('Adding new accessory:', device.name); - const accessory = new this.api.platformAccessory(device.name, uuid); + } else { + this.log.info('Adding new accessory:', device.name); + const accessory = new this.api.platformAccessory(device.name, uuid); - // initialize context information - accessory.context.device = device; + // initialize context information + accessory.context.device = device; - new WeatherFlowTempestPlatformAccessory(this, accessory); + new WeatherFlowTempestPlatformAccessory(this, accessory); - // link the accessory to the platform - this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); + // link the accessory to the platform + this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); - // add to array of active accessories - this.activeAccessory.push(accessory); + // add to array of active accessories + this.activeAccessory.push(accessory); + } } } diff --git a/src/platformAccessory.ts b/src/platformAccessory.ts index 964f0fd..57b819b 100644 --- a/src/platformAccessory.ts +++ b/src/platformAccessory.ts @@ -302,16 +302,27 @@ class Fan { try { const value_key: string = this.accessory.context.device.fan_properties.value_key; let speed: number = parseFloat(this.platform.observation_data[value_key]); - speed = Math.round(speed * 2.236936); // convert m/s to mph and round as fan % is integer value - if (speed > 100) { - this.platform.log.debug(`WeatherFlow Tempest is reporting wind speed exceeding 100mph: ${speed}mph`); - return 100; - } else if (speed < 0) { - this.platform.log.debug(`WeatherFlow Tempest is reporting wind speed less than 0mph: ${speed}mph`); - return 0; + if (this.platform.config.units === 'Metric') { + speed = Math.round(speed); // round as fan % is integer value + if (speed > 45) { + this.platform.log.debug(`WeatherFlow Tempest is reporting wind speed exceeding 45 m/s: ${speed} m/s`); + speed = 45; + } else if (speed < 0) { + this.platform.log.debug(`WeatherFlow Tempest is reporting wind speed less than 0 m/s: ${speed} m/s`); + speed = 0; + } } else { - return speed; + speed = Math.round(speed * 2.236936); // convert m/s to mph and round as fan % is integer value + if (speed > 100) { + this.platform.log.debug(`WeatherFlow Tempest is reporting wind speed exceeding 100 mph: ${speed} mph`); + speed = 100; + } else if (speed < 0) { + this.platform.log.debug(`WeatherFlow Tempest is reporting wind speed less than 0 mph: ${speed} mph`); + speed = 0; + } } + return speed; + } catch(exception) { this.platform.log.error(exception as string); return 0; diff --git a/src/tempestApi.ts b/src/tempest.ts similarity index 58% rename from src/tempestApi.ts rename to src/tempest.ts index 32606fb..727a2ef 100644 --- a/src/tempestApi.ts +++ b/src/tempest.ts @@ -1,10 +1,13 @@ import { Logger } from 'homebridge'; import axios, { AxiosResponse } from 'axios'; +import * as dgram from 'dgram'; import https from 'https'; + axios.defaults.timeout = 10000; // same as default interval axios.defaults.httpsAgent = new https.Agent({ keepAlive: true }); + export interface Observation { // temperature sensors air_temperature: number; // C, displayed according to Homebridge and HomeKit C/F settings @@ -30,6 +33,124 @@ export interface Observation { brightness: number; // Lux } + +export class TempestSocket { + + private log: Logger; + private s: dgram.Socket; + private data: object | undefined; + private tempest_battery_level: number; + + constructor(log: Logger) { + + this.log = log; + this.data = undefined; + this.tempest_battery_level = 0; + this.s = dgram.createSocket('udp4'); + + this.log.info('TempestSocket initialized.'); + + } + + public start(address = '0.0.0.0', port = 50222) { + + this.setupSocket(address, port); + this.setupSignalHandlers(); + + } + + private setupSocket(address: string, port: number) { + + this.s.bind({ address: address, port: port }); + this.s.on('message', (msg) => { + try { + const message_string = msg.toString('utf-8'); + const data = JSON.parse(message_string); + this.processReceivedData(data); + } catch (error) { + this.log.warn('JSON processing of data failed'); + this.log.error(error as string); + } + }); + + this.s.on('error', (err) => { + this.log.error('Socket error:', err); + }); + + } + + private processReceivedData(data) { + + if (data.type === 'obs_st') { // for Tempest + this.setTempestData(data); + } + + } + + private setTempestData(data): void { + + const obs = data.obs[0]; + // const windLull = (obs[1] !== null) ? obs[1] : 0; + const windSpeed = (obs[2] !== null) ? obs[2] * 2.2369 : 0; // convert to mph for heatindex calculation + const T = (obs[7] * 9/5) + 32; // T in F for heatindex, feelsLike and windChill calculations + + // eslint-disable-next-line max-len + const heatIndex = -42.379 + 2.04901523*T + 10.14333127*obs[8] - 0.22475541*T*obs[8] - 0.00683783*(T**2) - 0.05481717*(obs[8]**2) + 0.00122874*(T**2)*obs[8] + 0.00085282*T*(obs[8]**2) - 0.00000199*(T**2)*(obs[8]**2); + + // feels like temperature on defined for temperatures between 80F and 110F + const feelsLike = ((T >= 80) && (T <= 110)) ? heatIndex : T; + + // windChill only defined for wind speeds > 3 mph and temperature < 50F + const windChill = ((windSpeed > 3) && (T < 50)) ? (35.74 + 0.6215*T - 35.75*(windSpeed**0.16) + 0.4275*T*(windSpeed**0.16)) : T; + + this.data = { + air_temperature: obs[7], + feels_like: 5/9 * (feelsLike - 32), // convert back to C + wind_chill: 5/9 * (windChill - 32), // convert back to C + dew_point: obs[7] - ((100 - obs[8]) / 5.0), // Td = T - ((100 - RH)/5) + relative_humidity: obs[8], + wind_avg: obs[2], + wind_gust: obs[3], + barometric_pressure: obs[6], + precip: obs[12], + precip_accum_local_day: obs[12], + wind_direction: obs[4], + solar_radiation: obs[11], + uv: obs[10], + brightness: obs[9], + }; + this.tempest_battery_level = Math.round((obs[16] - 1.8) * 100); // 2.80V = 100%, 1.80V = 0% + + } + + private setupSignalHandlers(): void { + + process.on('SIGTERM', () => { + this.log.info('Got SIGTERM, shutting down Tempest Homebridge...'); + this.s.close(); + }); + + process.on('SIGINT', () => { + this.log.info('Got SIGINT, shutting down Tempest Homebridge...'); + this.s.close(); + }); + + } + + public hasData(): boolean { + return this.data !== undefined; + } + + public getStationCurrentObservation(): Observation { + return this.data as Observation; + } + + public getBatteryLevel(): number { + return this.tempest_battery_level; + } + +} + export class TempestApi { private log: Logger;