diff --git a/CHANGELOG.md b/CHANGELOG.md index d65c18a..b4f9281 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.2.0 +* Added the Lightning Strike Contact Sensor, allowing configuration of both the minimum distance and time thresholds for triggering CONTACT_NOT_DETECTED. + ## v4.1.1 * Update README.md to correctly display "Tempest" logo. * Update README.md to include `station_id` in "Local API Config Example". diff --git a/README.md b/README.md index 6eb4748..0a70be6 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,14 @@ [![verified-by-homebridge](https://badgen.net/badge/homebridge/verified/purple)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins) ![npm-version](https://badgen.net/npm/v/homebridge-weatherflow-tempest?icon=npm&label) ![npm-downloads](https://badgen.net/npm/dt/homebridge-weatherflow-tempest?icon=npm&label) [![donate](https://badgen.net/badge/donate/paypal/yellow)](https://paypal.me/chasenicholl) - - - - + + + +
+ + + +
*New* in v4.0.0 Local API Support! @@ -47,6 +51,7 @@ Local API is now supported which requires no authentication. If you choose to us - `sensors[].{1}_properties.value_key`: _(Required)_ Which REST API response body key to target for its value. You can find the available value_keys in the table below. - `sensors[].motion_properties.trigger_value`: _(Required with Motion Sensor)_ At what point (value) to trigger motion detected on/off. Minimum 1. - `sensors[].occupancy_properties.trigger_value`: _(Required with Occupancy Sensor)_ At what point (value) to trigger occupancy detected on/off. Minimum 0. +- `sensors[].contact_properties.trigger_distance`: _(Required with Contact Sensor)_ The minimum distance (in kilometers) at which the strike was detected to activate the contact sensor. `{1}` Replace with Sensor: temperature, humidity, light, fan @@ -166,6 +171,13 @@ sensor_type `{2}` | value_key | metric units | std units | additional_properties "value_key": "wind_direction", "trigger_value": 360 } + }, + { + "name": "Lightening Detector", + "sensor_type": "Contact Sensor", + "contact_properties": { + "trigger_distance": 10 + } } ], "platform": "WeatherFlowTempest" @@ -281,6 +293,13 @@ sensor_type `{2}` | value_key | metric units | std units | additional_properties "value_key": "wind_direction", "trigger_value": 360 } + }, + { + "name": "Lightening Detector", + "sensor_type": "Contact Sensor", + "contact_properties": { + "trigger_distance": 10 + } } ], "platform": "WeatherFlowTempest" diff --git a/config.schema.json b/config.schema.json index 22a3d15..067052c 100644 --- a/config.schema.json +++ b/config.schema.json @@ -3,211 +3,231 @@ "pluginType": "platform", "singular": false, "schema": { - "type": "object", - - "properties": { - "name": { + "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 + }, + "local_api_shared": { + "title": "Share the Local API UDP port with other processes", + "type": "boolean", + "required": false, + "default": false + }, + "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 of 5 digits)", + "type": "number", + "default": 0 + }, + "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, - "default": "WeatherFlow Tempest Platform" - }, - "local_api": { - "title": "Use Local API", - "type": "boolean", - "required": true, - "default": true - }, - "local_api_shared": { - "title": "Share the Local API UDP port with other processes", - "type": "boolean", - "required": false, - "default": false - }, - "token": { - "title": "Token", + "required": true + }, + "sensor_type": { "type": "string", - "default": "", + "enum": [ + "Temperature Sensor", + "Light Sensor", + "Humidity Sensor", + "Fan", + "Motion Sensor", + "Occupancy Sensor", + "Contact Sensor" + ], + "default": "Temperature Sensor" + }, + "fan_properties": { + "title": "Fan Properties", + "type": "object", "condition": { - "functionBody": "if (model.local_api != undefined && !model.local_api) { return true; } else { return false; };" + "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" + ] + } } - }, - "station_id": { - "title": "Station ID (Integer of 5 digits)", - "type": "number", - "default": 0 - }, - "interval": { - "title": "Interval (seconds)", - "type": "integer", - "default": 10, - "minimum": 1, + }, + "light_properties": { + "title": "Light Properties", + "type": "object", "condition": { - "functionBody": "if (model.local_api != undefined && !model.local_api) { return true; } else { return false; };" + "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" + ] + } } - }, - "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": 0, - "description": "At what point (value) to trigger occupancy detected on/off (0 minimum)." - } - } - } - } + }, + "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; };" }, - "required": [ - "name", - "sensor_type", - "value_key" - ] - } - }, - "required": ["local_api"], - "dependencies": { - "local_api": { - "not": { - "type": "boolean", - "const": true + "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; };" }, - "required": ["token", "station_id"] + "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": 0, + "description": "At what point (value) to trigger occupancy detected on/off (0 minimum)." + } + } + }, + "contact_properties": { + "title": "Contact Properties", + "type": "object", + "condition": { + "functionBody": "if (model.sensors[arrayIndices] && model.sensors[arrayIndices].sensor_type && model.sensors[arrayIndices].sensor_type === 'Contact Sensor') { return true; } else { return false; };" + }, + "description": "The Contact Sensor is utilized to trigger automations in response to lightning strikes.", + "properties": { + "trigger_distance": { + "type": "number", + "minimum": 0, + "default": 0, + "description": "The minimum distance (in kilometers) at which the strike was detected to activate the contact sensor." + } + } + } } + }, + "required": [ + "name", + "sensor_type", + "value_key" + ] + } + }, + "required": [ + "local_api" + ], + "dependencies": { + "local_api": { + "not": { + "type": "boolean", + "const": true + }, + "required": [ + "token", + "station_id" + ] } + } } -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 7fa6049..404c171 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "homebridge-weatherflow-tempest", - "version": "4.1.0", + "version": "4.2.3-beta.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "homebridge-weatherflow-tempest", - "version": "4.1.0", + "version": "4.2.3-beta.1.0", "license": "Apache-2.0", "dependencies": { "axios": "1.7.7" diff --git a/package.json b/package.json index 91bb6ab..31da732 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "private": false, "displayName": "Homebridge WeatherFlow Tempest", "name": "homebridge-weatherflow-tempest", - "version": "4.1.1", + "version": "4.2.3-beta.1.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/platform.ts b/src/platform.ts index 11f9e2b..13d1731 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -57,6 +57,8 @@ export class WeatherFlowTempestPlatform implements DynamicPlatformPlugin { feels_like: 0, wind_chill: 0, dew_point: 0, + lightning_strike_last_epoch: 0, + lightning_strike_last_distance: 0, }; this.tempest_battery_level = 0; diff --git a/src/platformAccessory.ts b/src/platformAccessory.ts index 57b819b..67a6414 100644 --- a/src/platformAccessory.ts +++ b/src/platformAccessory.ts @@ -603,6 +603,77 @@ class BatterySensor { } +class ContactSensor { + private service: Service; + private state: number; + constructor( + private readonly platform: WeatherFlowTempestPlatform, + private readonly accessory: PlatformAccessory, + ) { + + this.state = 0; + this.service = this.accessory.getService(this.platform.Service.ContactSensor) || + this.accessory.addService(this.platform.Service.ContactSensor); + + // Create handlers for required characteristics + this.service.getCharacteristic(this.platform.Characteristic.ContactSensorState) + .onGet(this.handleCurrentStateGet.bind(this)); + + // Set initial value + this.setCharacteristicState(0); // CONTACT_DETECTED + + // Update value based on a 1 second check interval + let tick = 0; + setInterval( () => { + tick++; + if (tick === 5) { // Reset Contact sensor every 5 seconds if CONTACT_NOT_DETECTED + tick = 0; + if (this.state === 1) { + this.setCharacteristicState(0); + } + } + this.setCharacteristicState(this.getState()); + }, 1000); + + } + + private getState(): number { + + try { + const lightning_strike_last_epoch: number = this.platform.observation_data.lightning_strike_last_epoch; + const lightning_strike_last_distance: number = this.platform.observation_data.lightning_strike_last_distance; + const trigger_distance: number = this.accessory.context.device.contact_properties.trigger_distance; + const current_epoch_now = Math.floor(Date.now() / 1000); + if (lightning_strike_last_epoch > 0 + && lightning_strike_last_distance > 0 + && lightning_strike_last_distance <= trigger_distance + && (current_epoch_now - lightning_strike_last_epoch) <= 5) { + return 1; // trigger CONTACT_NOT_DETECTED. + } + return 0; + } catch(exception) { + this.platform.log.error(exception as string); + return 0; + } + + } + + private setCharacteristicState(state: number): void { + + this.state = state; + this.service.getCharacteristic(this.platform.Characteristic.ContactSensorState).updateValue(state); + + } + + private handleCurrentStateGet(): number { + + this.platform.log.debug('Triggered GET handleCurrentStateGet for Contact Sensor state'); + return this.getState(); + + } + +} + /** * Initialize Tempest Platform (only need to do once) */ @@ -644,7 +715,6 @@ export class WeatherFlowTempestPlatformAccessory { switch (this.accessory.context.device.sensor_type) { case 'Temperature Sensor': new TemperatureSensor(this.platform, this.accessory); - // Add Battery to default Temperature air_temperature sensor if (this.accessory.context.device.temperature_properties.value_key === 'air_temperature') { new BatterySensor(this.platform, this.accessory); @@ -665,6 +735,9 @@ export class WeatherFlowTempestPlatformAccessory { case 'Occupancy Sensor': new OccupancySensor(this.platform, this.accessory); break; + case 'Contact Sensor': + new ContactSensor(this.platform, this.accessory); + break; } } diff --git a/src/tempest.ts b/src/tempest.ts index 15ba9c6..3006139 100644 --- a/src/tempest.ts +++ b/src/tempest.ts @@ -10,27 +10,30 @@ 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 + air_temperature: number; // C, displayed according to Homebridge and HomeKit C/F settings feels_like: number; wind_chill: number; dew_point: number; // humidity sensor - relative_humidity: number; // % + relative_humidity: number; // % // fan and motion sensor - wind_avg: number; // m/s, used for Fan speed % - wind_gust: number; // m/s, used for motion sensor + wind_avg: number; // m/s, used for Fan speed % + wind_gust: number; // m/s, used for motion sensor // occupancy sensors - barometric_pressure: number; // mbar - precip: number; // mm/min (minute sampling) - precip_accum_local_day: number; // mm - wind_direction: number; // degrees - solar_radiation: number; // W/m^2 - uv: number; // Index - - brightness: number; // Lux + barometric_pressure: number; // mbar + precip: number; // mm/min (minute sampling) + precip_accum_local_day: number; // mm + wind_direction: number; // degrees + solar_radiation: number; // W/m^2 + uv: number; // Index + + brightness: number; // Lux + + lightning_strike_last_epoch: number; // timestamp in seconds + lightning_strike_last_distance: number; // km } @@ -38,13 +41,30 @@ export class TempestSocket { private log: Logger; private s: dgram.Socket; - private data: object | undefined; + private data: Observation; private tempest_battery_level: number; constructor(log: Logger, reuse_address: boolean) { this.log = log; - this.data = undefined; + this.data = { + air_temperature: 0, + feels_like: 0, + wind_chill: 0, + dew_point: 0, + relative_humidity: 0, + wind_avg: 0, + wind_gust: 0, + barometric_pressure: 0, + precip: 0, + precip_accum_local_day: 0, + wind_direction: 0, + solar_radiation: 0, + uv: 0, + brightness: 0, + lightning_strike_last_epoch: 0, + lightning_strike_last_distance: 0, + }; this.tempest_battery_level = 0; this.s = dgram.createSocket({ type: 'udp4', reuseAddr: reuse_address }); @@ -81,15 +101,17 @@ export class TempestSocket { private processReceivedData(data) { - if (data.type === 'obs_st') { // for Tempest + if (data.type === 'obs_st') { // Observation event this.setTempestData(data); + } else if (data.type === 'evt_strike') { // Lightening strike event + this.appendStrikeEvent(data); } } - private setTempestData(data): void { + private setTempestData(event): void { - const obs = data.obs[0]; + const obs = event.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 @@ -103,26 +125,33 @@ export class TempestSocket { // 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.data.air_temperature = obs[7]; + this.data.feels_like = 5/9 * (feelsLike - 32); // convert back to C + this.data.wind_chill = 5/9 * (windChill - 32); // convert back to C + this.data.dew_point = obs[7] - ((100 - obs[8]) / 5.0); // Td = T - ((100 - RH)/5) + this.data.relative_humidity = obs[8]; + this.data.wind_avg = obs[2]; + this.data.wind_gust = obs[3]; + this.data.barometric_pressure = obs[6]; + this.data.precip = obs[12]; + this.data.precip_accum_local_day = obs[12]; + this.data.wind_direction = obs[4]; + this.data.solar_radiation = obs[11]; + this.data.uv = obs[10]; + this.data.brightness = obs[9]; this.tempest_battery_level = Math.round((obs[16] - 1.8) * 100); // 2.80V = 100%, 1.80V = 0% } + private appendStrikeEvent(data): void { + + if (this.data) { + this.data.lightning_strike_last_epoch = data.evt[0]; + this.data.lightning_strike_last_distance = data.evt[1]; + } + + } + private setupSignalHandlers(): void { process.on('SIGTERM', () => {