From 9632777c8e744b1d44c0b79de41211201f1a2ee5 Mon Sep 17 00:00:00 2001 From: chasenicholl Date: Wed, 1 Nov 2023 18:36:11 -0400 Subject: [PATCH 1/3] checkpoint. --- src/platform.ts | 2 +- src/{tempestApi.ts => tempest.ts} | 115 +++++++++++++++++++++++++++++ src/test.ts | 119 ++++++++++++++++++++++++++++++ src/tester.py | 30 ++++++++ test.py | 72 ++++++++++++++++++ 5 files changed, 337 insertions(+), 1 deletion(-) rename src/{tempestApi.ts => tempest.ts} (63%) create mode 100644 src/test.ts create mode 100644 src/tester.py create mode 100644 test.py diff --git a/src/platform.ts b/src/platform.ts index 1271af6..8a5b50b 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -3,7 +3,7 @@ import { API, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig, import { PLATFORM_NAME, PLUGIN_NAME } from './settings'; import { WeatherFlowTempestPlatformAccessory } from './platformAccessory'; -import { TempestApi, Observation } from './tempestApi'; +import { TempestApi, Observation } from './tempest'; interface TempestSensor { name: string; diff --git a/src/tempestApi.ts b/src/tempest.ts similarity index 63% rename from src/tempestApi.ts rename to src/tempest.ts index 9db652e..31698b5 100644 --- a/src/tempestApi.ts +++ b/src/tempest.ts @@ -1,5 +1,6 @@ import { Logger } from 'homebridge'; import axios, { AxiosResponse } from 'axios'; +import * as dgram from 'dgram'; export interface Observation { // temperature sensors @@ -26,6 +27,120 @@ export interface Observation { brightness: number; // Lux } +export interface SocketObservation { + + timestamp: number; + windLull: number; + windSpeed: number; + windGust: number; + windDirection: number; + pressure: number; + temperature: number; + humidity: number; + illumination: number; + uvIndex: number; + solarRadiation: number; + rain: number; + strikes: number; + lightningDistance: number; + reportingInterval: number; + +} + + +export class TempestSocket { + + private log: Logger; + private s: dgram.Socket; + + constructor(log: Logger, address = '0.0.0.0', port = 50222) { + + this.log = log; + this.s = dgram.createSocket('udp4'); + this.setupSocket(address, port); + this.setupSignalHandlers(); + } + + private setupSocket(address: string, port: number) { + + // this.s.setsockopt(dgram.SOL_SOCKET, dgram.SO_REUSEADDR, 1); + // this.s.setsockopt(dgram.SOL_SOCKET, dgram.SO_REUSEPORT, 1); + 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: any) { + // if (data.type === 'obs_air') { + // console.log(data); + // // air_tm = this.air_data(data, air_tm); + // } + + if (data.type === 'obs_st') { + // console.log(data); + this.parseTempestData(data); + // st_tm = this.tempest_data(data, st_tm); + } + + // if (data.type === 'obs_sky') { + // console.log(data); + // // sky_tm = this.sky_data(data, sky_tm); + // } + } + + private parseTempestData(data: any): SocketObservation { + const obs = data.obs[0]; + const windLull = (obs[1] !== null) ? obs[1] * 2.2369 : 0; + const windSpeed = (obs[2] !== null) ? obs[2] * 2.2369 : 0; + const windGust = (obs[3] !== null) ? obs[3] * 2.2369 : 0; + return { + timestamp: obs[0], + windLull: windLull, + windSpeed: windSpeed, + windGust: windGust, + windDirection: obs[4], + pressure: obs[6], + temperature: obs[7], + humidity: obs[8], + illumination: obs[9], + uvIndex: obs[10], + solarRadiation: obs[11], + rain: parseFloat(obs[12]), + strikes: obs[14], + lightningDistance: obs[15], + reportingInterval: obs[17], + } as SocketObservation; + + } + + private setupSignalHandlers() { + + process.on('SIGTERM', () => { + this.log.info('Got SIGTERM, shutting down Tempest Homebridge...'); + }); + + process.on('SIGINT', () => { + this.log.info('Got SIGINT, shutting down Tempest Homebridge...'); + this.s.close(); + }); + + } + +} + export class TempestApi { private log: Logger; diff --git a/src/test.ts b/src/test.ts new file mode 100644 index 0000000..23cb813 --- /dev/null +++ b/src/test.ts @@ -0,0 +1,119 @@ +import * as dgram from 'dgram'; + +export interface SocketObservation { + + timestamp: number; + windLull: number; + windSpeed: number; + windGust: number; + windDirection: number; + pressure: number; + temperature: number; + humidity: number; + illumination: number; + uvIndex: number; + solarRadiation: number; + rain: number; + strikes: number; + lightningDistance: number; + reportingInterval: number; + +} + + +export class TempestSocket { + + // private log: Logger; + private s: dgram.Socket; + + constructor(address = '0.0.0.0', port = 50222) { + + // this.log = log; + this.s = dgram.createSocket('udp4'); + this.setupSocket(address, port); + this.setupSignalHandlers(); + } + + private setupSocket(address: string, port: number) { + + // this.s.setsockopt(dgram.SOL_SOCKET, dgram.SO_REUSEADDR, 1); + // this.s.setsockopt(dgram.SOL_SOCKET, dgram.SO_REUSEPORT, 1); + 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); + console.log(data); + this.processReceivedData(data); + } catch (error) { + console.log('JSON processing of data failed'); + console.log(error as string); + } + }); + + this.s.on('error', (err) => { + console.log('Socket error:', err); + }); + + } + + private processReceivedData(data: any) { + // if (data.type === 'obs_air') { + // console.log(data); + // // air_tm = this.air_data(data, air_tm); + // } + + if (data.type === 'obs_st') { + // console.log(data); + const parsed_data = this.parseTempestData(data); + console.log(parsed_data); + // st_tm = this.tempest_data(data, st_tm); + } + + // if (data.type === 'obs_sky') { + // console.log(data); + // // sky_tm = this.sky_data(data, sky_tm); + // } + } + + private parseTempestData(data: any): SocketObservation { + const obs = data.obs[0]; + const windLull = (obs[1] !== null) ? obs[1] * 2.2369 : 0; + const windSpeed = (obs[2] !== null) ? obs[2] * 2.2369 : 0; + const windGust = (obs[3] !== null) ? obs[3] * 2.2369 : 0; + return { + timestamp: obs[0], + windLull: windLull, + windSpeed: windSpeed, + windGust: windGust, + windDirection: obs[4], + pressure: obs[6], + temperature: obs[7], + humidity: obs[8], + illumination: obs[9], + uvIndex: obs[10], + solarRadiation: obs[11], + rain: parseFloat(obs[12]), + strikes: obs[14], + lightningDistance: obs[15], + reportingInterval: obs[17], + } as SocketObservation; + + } + + private setupSignalHandlers() { + + process.on('SIGTERM', () => { + console.log('Got SIGTERM, shutting down Tempest Homebridge...'); + }); + + process.on('SIGINT', () => { + console.log('Got SIGINT, shutting down Tempest Homebridge...'); + this.s.close(); + }); + + } + +} + +new TempestSocket(); \ No newline at end of file diff --git a/src/tester.py b/src/tester.py new file mode 100644 index 0000000..7a06a7e --- /dev/null +++ b/src/tester.py @@ -0,0 +1,30 @@ +s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) +s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) +s.bind(("0.0.0.0", 50222)) + +stopped = False +try: + while not stopped: + try: + hub = s.recvfrom(1024) + data = json.loads(hub[0].decode("utf-8")) # hub is a truple (json, ip, port) + except json.JSONDecodeError: + print("JSON processing of data failed") + continue + + if (data["type"] == "obs_air"): + print(data) + # air_tm = self.air_data(data, air_tm) + + if (data["type"] == "obs_st"): + print(data) + parse_tempest_data(data) + # st_tm = self.tempest_data(data, st_tm) + + if (data["type"] == "obs_sky"): + print(data) + # sky_tm = self.sky_data(data, sky_tm) +except KeyboardInterrupt: + print("Keyboard Interupt") +s.close() \ No newline at end of file diff --git a/test.py b/test.py new file mode 100644 index 0000000..876b1ea --- /dev/null +++ b/test.py @@ -0,0 +1,72 @@ +import json +import socket + + +def open_socket_connection(): + + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + s.bind(("0.0.0.0", 50222)) + + stopped = False + try: + while not stopped: + try: + hub = s.recvfrom(1024) + data = json.loads(hub[0].decode("utf-8")) # hub is a truple (json, ip, port) + except json.JSONDecodeError: + print("JSON processing of data failed") + continue + + if (data["type"] == "obs_air"): + print(data) + # air_tm = self.air_data(data, air_tm) + + if (data["type"] == "obs_st"): + print(data) + parse_tempest_data(data) + # st_tm = self.tempest_data(data, st_tm) + + if (data["type"] == "obs_sky"): + print(data) + # sky_tm = self.sky_data(data, sky_tm) + except KeyboardInterrupt: + print("Keyboard Interupt") + s.close() + + +def parse_tempest_data(data): + timestamp = data['obs'][0][0] # ts + # convert wind speed from m/s to MPH + if (data["obs"][0][1] is not None): + wind_lull = data["obs"][0][1] * 2.2369 # wind lull + else: + wind_lull = 0 + if (data["obs"][0][2] is not None): + wind_speed = data["obs"][0][2] * 2.2369 # wind speed + else: + wind_speed = 0 + if (data["obs"][0][3] is not None): + wind_gust = data["obs"][0][3] * 2.2369 # wind gust + else: + wind_gust = 0 + wind_direction = data['obs'][0][4] # wind direction + pressure = data['obs'][0][6] # pressure + temperature = data['obs'][0][7] # temp + humidity = data['obs'][0][8] # humidity + illumination = data['obs'][0][9] # Illumination + uv_index = data['obs'][0][10] # UV Index + solar_radiation = data['obs'][0][11] # solar radiation + rain = float(data['obs'][0][12]) # rain + strikes = data['obs'][0][14] # strikes + lightening_distance = data['obs'][0][15] # distance + reporting_interval = data['obs'][0][17] # reporting interval + + +def main(): + open_socket_connection() + + +if __name__ == "__main__": + main() \ No newline at end of file From ee27de5751f6e2504872f47f761ca55da38216f3 Mon Sep 17 00:00:00 2001 From: chasenicholl Date: Thu, 2 Nov 2023 14:28:22 -0400 Subject: [PATCH 2/3] update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af3019b..95e77f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ 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! Optionally 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. +* 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. From f8ab3f599553d3a3f7517f4a69861656692de1ff Mon Sep 17 00:00:00 2001 From: chasenicholl Date: Thu, 2 Nov 2023 14:56:47 -0400 Subject: [PATCH 3/3] updated wait function --- src/platform.ts | 46 +++++++++++++++++++++++++++++----------------- src/tempest.ts | 4 ++-- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/platform.ts b/src/platform.ts index ce865c0..f5dd0a1 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -61,15 +61,13 @@ export class WeatherFlowTempestPlatform implements DynamicPlatformPlugin { this.tempest_battery_level = 0; this.tempest_device_id = 0; - if (this.config.local_api === false) { - // Make sure the Station ID is the integer ID - if (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//', - ); - return; - } + // Make sure the Station ID is the integer 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//', + ); + return; } this.api.on('didFinishLaunching', () => { @@ -92,20 +90,15 @@ export class WeatherFlowTempestPlatform implements DynamicPlatformPlugin { } - private initializeBySocket() { + private async initializeBySocket() { try { this.log.info('Using Tempest Local API.'); this.tempestSocket = new TempestSocket(this.log); this.tempestSocket.start(); - // Hold thread for first message. - this.log.info('Waiting for first local broadcast. This could take up to 60 seconds...'); - while (!this.tempestSocket.hasData()) { - continue; - } - this.log.info('Local broadcast recieved.'); - // Set values + // Hold thread for first message and set values + await this.socketDataRecieved(); this.observation_data = this.tempestSocket.getStationCurrentObservation(); this.tempest_battery_level = this.tempestSocket.getBatteryLevel(); @@ -116,11 +109,30 @@ export class WeatherFlowTempestPlatform implements DynamicPlatformPlugin { // Poll every minute for local API this.pollLocalStationCurrentObservation(); + } 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() { try { diff --git a/src/tempest.ts b/src/tempest.ts index 853947f..df8c838 100644 --- a/src/tempest.ts +++ b/src/tempest.ts @@ -79,7 +79,7 @@ export class TempestSocket { } - private processReceivedData(data: any) { + private processReceivedData(data) { if (data.type === 'obs_st') { this.setTempestData(data); @@ -87,7 +87,7 @@ export class TempestSocket { } - private setTempestData(data: any): void { + private setTempestData(data): void { const obs = data.obs[0]; // const windLull = (obs[1] !== null) ? obs[1] * 2.2369 : 0;