UNPKG

iobroker.weatherflow_udp

Version:
1,087 lines (924 loc) 50.8 kB
/* * Created with @iobroker/create-adapter v1.25.0 */ // The adapter-core module gives you access to the core ioBroker functions // you need to create an adapter const utils = require('@iobroker/adapter-core'); // Load your modules here, e.g.: const dgram = require('dgram'); // If radiation is more than 120 W/m2 it is counted as sunshine (https://de.wikipedia.org/wiki/Sonnenschein) const SUNSHINETHRESHOLD = 120; // const as min max function parameter const MIN = 1; const MAX = 2; let mServer = null; let timer; let now = new Date(); // set as system time for now, will be overwritten if timestamp is recieved let oldNow = new Date(); // set as system time for now, will be overwritten if timestamp is recieved const existingStates = []; // Import constants with static interpretation data const { devices, messages, windDirections, minCalcs, maxCalcs, sensorfails, powermodes, } = require(`${__dirname}/lib/messages`); class WeatherflowUdp extends utils.Adapter { /** * @param {Partial<utils.AdapterOptions>} [options={}] */ constructor(options) { super({ ...options, name: 'weatherflow_udp', }); this.on('ready', this.onReady.bind(this)); this.on('unload', this.onUnload.bind(this)); } /** * Is called when databases are connected and adapter received configuration. */ onReady() { // Initialize your adapter here this.main(); } async main() { const that = this; mServer = dgram.createSocket('udp4'); // Attach to UDP Port try { mServer.bind(this.config.UDP_port, '0.0.0.0'); } catch (e) { that.log.error(['Could not bind to port: ', this.config.UDP_port, '. Adapter stopped.'].join('')); } mServer.on('error', (err) => { this.log.error(`Cannot open socket:\n${err.stack}`); mServer.close(); timer = setTimeout(() => process.exit(), 1000); // delay needed to wait for logging }); // Reset the connection indicator during startup this.setState('info.connection', false, true); mServer.on('listening', () => { const address = mServer.address(); that.log.info(`adapter listening ${address.address}:${address.port}`); }); // Receive UDP message mServer.on('message', (messageString, rinfo) => { let message; // JSON parsed message if (that.config.debug === true) { that.log.debug(`${rinfo.address}:${rinfo.port} - ${messageString.toString('ascii')}`); } try { message = JSON.parse(messageString.toString()); } catch (e) { // Anweisungen für jeden Fehler that.log.warn(['Non-JSON message received: "', message, '". Ignoring.'].join('')); return; } // stop processing if message does not have a type if ('type' in message === false) { that.log.warn(['Non- or unknown weatherflow message received: "', message, '". Ignoring.'].join('')); return; } // Set connection state when message received and expire after 6 minutes of inactivity that.setStateAsync('info.connection', { val: true, ack: true, expire: 360 }); const messageType = message.type; // e.g. 'rapid_wind' if (that.config.debug === true) { that.log.info(['Message type: "', message.type, '"'].join('')); } const messageInfo = messages[messageType]; if (that.config.debug === true) { that.log.info(['messageInfo: ', JSON.stringify(messageInfo)].join('')); } let statePath; // name of current state to set or create if (!messageInfo) { if (that.config.debug === true) { that.log.info(['Unknown message type: ', messageType, ' - ignoring'].join('')); } } else { if (that.config.debug === true) { that.log.info(['messageInfo: ', JSON.stringify(messageInfo)].join('')); } if ('serial_number' in message) { // create structure for device // Get type from first 2 characters of serial number const deviceType = devices[message.serial_number.substring(0, 2)]; const serialParameters = { type: 'device', common: { name: `${deviceType}: ${message.serial_number}`, }, native: {}, }; if ('hub_sn' in message) { // device message with serial and hub serial // Create state for hub // Type is first 2 chars of serial number const hubType = devices[message.hub_sn.substring(0, 2)]; const hubSnParameters = { type: 'device', common: { name: `${hubType}:${message.hub_sn}`, }, native: {}, }; that.myCreateState(message.hub_sn, hubSnParameters); // create device that.myCreateState([message.hub_sn, message.serial_number].join('.'), serialParameters); // create device // Set complete path to state hub and serial statePath = [message.hub_sn, message.serial_number, message.type].join('.'); } else { // device message without hub serial (probably only hub) that.myCreateState(message.serial_number, serialParameters); // create device // Set path to state statePath = [message.serial_number, '.', message.type].join(''); } } if (that.config.debug) { that.log.info(['statepath: ', statePath].join('')); } // Write last message to the node lastMessage in statepath const lastMessageParameter = { type: 'state', common: { name: 'Last message on this channel', type: 'string', role: 'text', read: true, write: false, }, native: {}, }; that.myCreateState([statePath, 'lastMessage'].join('.'), lastMessageParameter, messageString.toString('ascii')); // create/update lestMessage state per message type // Walk through items of message Object.keys(message).forEach((item) => { let itemvalue = []; if (typeof message[item][0] === 'object') { // some items like 'obs' are double arrays [[]], remove outer array itemvalue = message[item][0]; } else if (typeof message[item] === 'object') { // some are arrays, take as is itemvalue = message[item]; } else if (typeof message[item] === 'number' || typeof message[item] === 'string') { // others are just numbers or strings, then wrap into an array itemvalue.push(message[item]); } if (that.config.debug) { that.log.info(['item: ', item, ' = ', itemvalue].join('')); } // Set some Items to be ignored later as they are parsed differently const ignoreItems = ['type', 'serial_number', 'hub_sn']; // Check for unknown/new items if ((item in messageInfo) === false && ignoreItems.includes(item) === false) { that.log.warn(['Message ', messageType, ' contains unknown parameter: ', item, ' = ', itemvalue, '. Ignoring. Please check UDP message version and check with adapter developer.'].join('')); } // only parse if part of 'states' definition if (messageInfo[item] !== null && ignoreItems.includes(item) === false) { // Walk through fields 0 ... n Object.keys(itemvalue).forEach(async (field) => { if (!messageInfo[item][field]) { that.log.warn(['Message contains unknown field "(', field, '" in message ', item, ')". Check UDP message version and inform adapter developer.'].join('')); return; } const pathParameters = { type: 'channel', common: { name: messageInfo.name, }, native: {}, }; const stateParameters = messageInfo[item][field][1]; const stateName = [statePath, messageInfo[item][field][0]].join('.'); let fieldvalue = itemvalue[field]; // Deal with timestamp messages if (messageInfo[item][field][0] === 'timestamp') { fieldvalue = new Date(fieldvalue * 1000).getTime(); // timestamp in iobroker is milliseconds and provided timestamp is seconds } if (that.config.debug === true) { that.log.info(['[', field, '] ', 'state: ', stateName, ' = ', fieldvalue].join('')); } // handle timestamp old and new as now and oldNow // used later if (messageInfo[item][field][0] === 'timestamp') { now = new Date(fieldvalue); // now is date/time of current message const obj = await that.getValObj(stateName); if (obj !== null) { oldNow = new Date(obj.val); } } // Special corrections on data //= ===================================== if (messageInfo[item][field][0] === 'lightningStrikeAvgDistance' && fieldvalue === 0) { // If average lightning distance is zero, no lightning was detected, set to 999 to mark this fact fieldvalue = 999; } // Walkaround for for occasional 0-pressure values if (messageInfo[item][field][0] === 'stationPressure' && fieldvalue === 0) { return; // skip value if this happens } // Calculate minimum values of today and yesterday for native values // Min-values if (minCalcs.includes(messageInfo[item][field][0])) { that.calcMinMaxValue(stateName, stateParameters, fieldvalue, MIN); } // Max-values if (maxCalcs.includes(messageInfo[item][field][0])) { that.calcMinMaxValue(stateName, stateParameters, fieldvalue, MAX); } // And update states //= ============ that.myCreateState(statePath, pathParameters); // create channel that.myCreateState(stateName, stateParameters, fieldvalue); // create node //= ===================================== // Do special tasks based on message type //= ===================================== // set a state for rain intensity // ------------------------------ // NONE: 0 mm / hour // VERY LIGHT: > 0, < 0.25 mm / hour // LIGHT: ≥ 0.25, < 1.0 mm / hour // MODERATE: ≥ 1.0, < 4.0 mm / hour // HEAVY: ≥ 4.0, < 16.0 mm / hour // VERY HEAVY: ≥ 16.0, < 50 mm / hour // EXTREME: > 50.0 mm / hour if (messageInfo[item][field][0] === 'precipAccumulated') { const stateNameRainIntensity = [statePath, 'rainIntensity'].join('.'); const stateParametersRainIntensity = { type: 'state', common: { type: 'mixed', states: { 0: 'none', 1: 'very light', 2: 'light', 3: 'moderate', 4: 'heavy', 5: 'very heavy', 6: 'extreme', }, read: true, write: false, role: 'value.precipitation.level', name: 'Rain intensity; adapter calculated', }, native: {}, }; const reportIntervalName = [statePath, 'reportInterval'].join('.'); let rainIntensity = 0; const reportInterval = await that.getValObj(reportIntervalName); if (reportInterval !== null) { if ((fieldvalue * 60) / reportInterval.val > 50) { rainIntensity = 6; } else if ((fieldvalue * 60) / reportInterval.val > 16) { rainIntensity = 5; } else if ((fieldvalue * 60) / reportInterval.val > 4) { rainIntensity = 4; } else if ((fieldvalue * 60) / reportInterval.val > 1) { rainIntensity = 3; } else if ((fieldvalue * 60) / reportInterval.val > 0.25) { rainIntensity = 2; } else if ((fieldvalue * 60) / reportInterval.val > 0) { rainIntensity = 1; } that.myCreateState(stateNameRainIntensity, stateParametersRainIntensity, rainIntensity); } } // raining or not as boolean state? //------------------------------- if (messageInfo[item][field][0] === 'precipAccumulated') { const statePathCorrected = statePath.replace('obs_st', 'evt_precip').replace('obs_sky', 'evt_precip'); // move state from observation to evt_precip const stateNameRaining = [statePathCorrected, 'raining'].join('.'); const stateParametersRaining = { type: 'state', common: { type: 'boolean', read: true, write: false, role: 'indicator.rain', name: 'Raining; adapter calculated', def: false, }, native: {}, }; if (fieldvalue > 0) { that.myCreateState(stateNameRaining, stateParametersRaining, true); } else { that.myCreateState(stateNameRaining, stateParametersRaining, false); } } if (messageType === 'evt_precip' && messageInfo[item][field][0] === 'timestamp') { // if precipitation start is received also set to true const stateNameRaining = [statePath, 'raining'].join('.'); const stateParametersRaining = { type: 'state', common: { type: 'boolean', read: true, write: false, role: 'indicator.rain', name: 'Raining; adapter calculated', def: false, }, native: {}, }; that.myCreateState(stateNameRaining, stateParametersRaining, true); } // rain accumulation and time of current and previous hour //------------------------------------------------------- if (messageInfo[item][field][0] === 'precipAccumulated') { // rain amount // ----------- const stateNameCurrentHourA = [statePath, 'precipAccumulatedCurrentHour'].join('.'); const stateParametersCurrentHourA = { type: 'state', common: { type: 'number', unit: 'mm', read: true, write: false, role: 'value.precipitation', name: 'Accumulated rain in current hour; adapter calculated', }, native: {}, }; const stateNamePreviousHourA = [statePath, 'precipAccumulatedPreviousHour'].join('.'); const stateParametersPreviousHourA = { type: 'state', common: { type: 'number', unit: 'mm', read: true, write: false, role: 'value.precipitation', name: 'Accumulated rain in previous hour; adapter calculated', }, native: {}, }; const stateNameTodayA = [statePath, 'precipAccumulatedToday'].join('.'); const stateParametersTodayA = { type: 'state', common: { type: 'number', unit: 'mm', read: true, write: false, role: 'value.precipitation', name: 'Accumulated rain today; adapter calculated', }, native: {}, }; const stateNameYesterdayA = [statePath, 'precipAccumulatedYesterday'].join('.'); const stateParametersYesterdayA = { type: 'state', common: { type: 'number', unit: 'mm', read: true, write: false, role: 'value.precipitation', name: 'Accumulated rain yesterday; adapter calculated', }, native: {}, }; let newValueHourA = 0; let newValueDayA = 0; // hour const objhourA = await this.getValObj(stateNameCurrentHourA); // get old value if (objhourA !== null) { if (now.getHours() === oldNow.getHours()) { // same hour newValueHourA = objhourA.val + fieldvalue; // add } else { // different hour newValueHourA = fieldvalue; // replace that.myCreateState(stateNamePreviousHourA, stateParametersPreviousHourA, objhourA.val); // save value from current hour to last hour } } that.myCreateState(stateNameCurrentHourA, stateParametersCurrentHourA, newValueHourA); // always write value for current hour // day const objdayA = await this.getValObj(stateNameTodayA); // get old value if (objdayA !== null) { if (now.getDay() === oldNow.getDay()) { // same hour newValueDayA = objdayA.val + fieldvalue; // add } else { // different hour newValueDayA = fieldvalue; // replace that.myCreateState(stateNameYesterdayA, stateParametersYesterdayA, objdayA.val); // save value from current day to yesterday } } that.myCreateState(stateNameTodayA, stateParametersTodayA, newValueDayA); // always write value for current day // rain duration // ------------- const stateNameCurrentHourD = [statePath, 'precipDurationCurrentHour'].join('.'); const stateParametersCurrentHourD = { type: 'state', common: { type: 'number', unit: 'min', read: true, write: false, role: 'value.precipitation.duration', name: 'Rain duration in current hour; adapter calculated', }, native: {}, }; const stateNamePreviousHourD = [statePath, 'precipDurationPreviousHour'].join('.'); const stateParametersPreviousHourD = { type: 'state', common: { type: 'number', unit: 'min', read: true, write: false, role: 'value.precipitation.duration', name: 'Rain duration in previous hour; adapter calculated', }, native: {}, }; const stateNameTodayD = [statePath, 'precipDurationToday'].join('.'); const stateParametersTodayD = { type: 'state', common: { type: 'number', unit: 'h', read: true, write: false, role: 'value.precipitation.duration', name: 'Rain duration today; adapter calculated', }, native: {}, }; const stateNameYesterdayD = [statePath, 'precipDurationYesterday'].join('.'); const stateParametersYesterdayD = { type: 'state', common: { type: 'number', unit: 'h', read: true, write: false, role: 'value.precipitation.duration', name: 'Rain duration yesterday; adapter calculated', }, native: {}, }; const reportIntervalNameD = [statePath, 'reportInterval'].join('.'); let newValueHourD = 0; let newValueDayD = 0; // hour const objhourD = await this.getValObj(stateNameCurrentHourD); // get old value const reportIntervalD = await this.getValObj(reportIntervalNameD); // get report Interval for multiplication if (objhourD !== null && reportIntervalD !== null) { if (now.getHours() === oldNow.getHours()) { // same hour if (fieldvalue > 0) { newValueHourD = objhourD.val + reportIntervalD.val; // add } else { newValueHourD = objhourD.val; // no change } } else { // different hour if (fieldvalue > 0) { newValueHourD = reportIntervalD.val; // replace } else { newValueHourD = 0; // reset todays value } that.myCreateState(stateNamePreviousHourD, stateParametersPreviousHourD, objhourD.val); // save value from current hour to last hour } } that.myCreateState(stateNameCurrentHourD, stateParametersCurrentHourD, newValueHourD); // always write value for current hour const objdayD = await this.getValObj(stateNameTodayD); // get old value if (objdayD !== null && reportIntervalD !== null) { if (now.getDay() === oldNow.getDay()) { // same day if (fieldvalue > 0) { newValueDayD = objdayD.val + reportIntervalD.val / 60; // add in hours } else { newValueDayD = objdayD.val; } } else { // different day if (fieldvalue > 0) { newValueDayD = reportIntervalD.val / 60; // replace } else { newValueDayD = 0; } that.myCreateState(stateNameYesterdayD, stateParametersYesterdayD, objdayD.val); // save value from current day to last yesterday } } that.myCreateState(stateNameTodayD, stateParametersTodayD, newValueDayD); // always write value for current day } // sunshine duration of previous and current hour, today and last day //------------------------------------------------------------------ if (messageInfo[item][field][0] === 'solarRadiation') { // sunshine duration const stateNameCurrentHour = [statePath, 'sunshineDurationCurrentHour'].join('.'); const stateParametersCurrentHour = { type: 'state', common: { type: 'number', unit: 'min', read: true, write: false, role: 'value.sunshine', name: 'Sunshine duration in current hour; adapter calculated', }, native: {}, }; const stateNamePreviousHour = [statePath, 'sunshineDurationPreviousHour'].join('.'); const stateParametersPreviousHour = { type: 'state', common: { type: 'number', unit: 'min', read: true, write: false, role: 'value.sunshine', name: 'Sunshine duration in previous hour; adapter calculated', }, native: {}, }; const stateNameToday = [statePath, 'sunshineDurationToday'].join('.'); const stateParametersToday = { type: 'state', common: { type: 'number', unit: 'h', read: true, write: false, role: 'value.sunshine', name: 'Sunshine duration today; adapter calculated', }, native: {}, }; const stateNameYesterday = [statePath, 'sunshineDurationYesterday'].join('.'); const stateParametersYesterday = { type: 'state', common: { type: 'number', unit: 'h', read: true, write: false, role: 'value.sunshine', name: 'Sunshine duration yesterday; adapter calculated', }, native: {}, }; const reportIntervalName = [statePath, 'reportInterval'].join('.'); // hour let newValueHour = 0; let newValueDay = 0; const objhour = await that.getValObj(stateNameCurrentHour); // get old value const reportInterval = await that.getValObj(reportIntervalName); if (objhour !== null && reportInterval !== null) { if (now.getHours() === oldNow.getHours()) { // same hour if (fieldvalue >= SUNSHINETHRESHOLD) { newValueHour = objhour.val + reportInterval.val; // add } } else { // different hour if (fieldvalue >= SUNSHINETHRESHOLD) { newValueHour = reportInterval.val; // replace } else { newValueHour = 0; } that.myCreateState(stateNamePreviousHour, stateParametersPreviousHour, objhour.val); // save value from current hour to last hour } } that.myCreateState(stateNameCurrentHour, stateParametersCurrentHour, newValueHour); // always write value for current hour // day const objday = await that.getValObj(stateNameToday); // get old value if (objday !== null && reportInterval !== null) { if (now.getDay() === oldNow.getDay()) { // same day if (fieldvalue >= SUNSHINETHRESHOLD) { newValueDay = objday.val + reportInterval.val / 60; // add } else { newValueDay = objday.val; } } else { // different hour if (fieldvalue >= SUNSHINETHRESHOLD) { newValueDay = reportInterval.val / 60; // replace } else { newValueDay = 0; } that.myCreateState(stateNameYesterday, stateParametersYesterday, objday.val); // save value from current day to last yesterday } } that.myCreateState(stateNameToday, stateParametersToday, newValueDay); // always write value for current day } // Set a state sunshine to true, if above threshold //------------------------------------------------ if (messageInfo[item][field][0] === 'solarRadiation') { const stateNameSunshine = [statePath, 'sunshine'].join('.'); const stateParametersSunshine = { type: 'state', common: { type: 'boolean', read: true, write: false, role: 'indicator.sunshine', name: 'Sunshine (> 120 W/m2); adapter calculated', }, native: {}, }; if (fieldvalue >= SUNSHINETHRESHOLD) { that.myCreateState(stateNameSunshine, stateParametersSunshine, true); } else { that.myCreateState(stateNameSunshine, stateParametersSunshine, false); } } // Reduced pressure (sea level) from station pressure //-------------------------------------------------- if (messageInfo[item][field][0] === 'stationPressure') { let airTemperature = 15; // standard value if not available let relativeHumidity = 50; // standard value if not available const stateNameAirTemperature = [statePath, 'airTemperature'].join('.'); const stateNameRelativeHumidity = [statePath, 'relativeHumidity'].join('.'); const stateNameReducedPressure = [statePath, 'reducedPressure'].join('.'); const stateParametersReducedPressure = { type: 'state', common: { type: 'number', unit: 'hPa', read: true, write: false, role: 'value.pressure', name: 'Reduced pressure (sea level); adapter calculated', }, native: {}, }; const obj = await that.getValObj(stateNameAirTemperature); const obj1 = await that.getValObj(stateNameRelativeHumidity); if (obj !== null && obj1 !== null) { airTemperature = obj.val; relativeHumidity = obj1.val; const reducedPressure = getQFF(airTemperature, fieldvalue, that.config.height, relativeHumidity); // Calculate min/max for reduced pressure that.calcMinMaxValue(stateNameReducedPressure, stateParametersReducedPressure, reducedPressure, MIN); that.calcMinMaxValue(stateNameReducedPressure, stateParametersReducedPressure, reducedPressure, MAX); that.myCreateState(stateNameReducedPressure, stateParametersReducedPressure, reducedPressure); if (that.config.debug) { that.log.info(['Pressure conversion: ', 'Station pressure: ', fieldvalue, ', Height: ', that.config.height, ', Temperature: ', airTemperature, ', Humidity: ', relativeHumidity, ', Reduced pressure: ', reducedPressure].join('')); } } } // Dewpoint from temperature and humidity //---------------------------------------------- // Is calculated and written when humidity is received (temperature comes before that, so it should be current) if (messageInfo[item][field][0] === 'relativeHumidity') { const stateNameAirTemperature = [statePath, 'airTemperature'].join('.'); const stateNameDewpoint = [statePath, 'dewpoint'].join('.'); const stateParametersDewpoint = { type: 'state', common: { type: 'number', unit: '°C', read: true, write: false, role: 'value.temperature.dewpoint', name: 'Dewpoint; adapter calculated', }, native: {}, }; const obj = await that.getValObj(stateNameAirTemperature); if (obj !== null) { const airTemperature = obj.val; // Calculate min/max for dewpoint that.calcMinMaxValue(stateNameDewpoint, stateParametersDewpoint, dewpoint(airTemperature, fieldvalue).dewpointTemp, MIN); that.calcMinMaxValue(stateNameDewpoint, stateParametersDewpoint, dewpoint(airTemperature, fieldvalue).dewpointTemp, MAX); that.myCreateState(stateNameDewpoint, stateParametersDewpoint, dewpoint(airTemperature, fieldvalue).dewpointTemp); } } // absolute Humidity from temperature, humidity and station pressure //---------------------------------------------- // Is calculated and written when humidity is received (temperature comes before that, so it should be current) if (messageInfo[item][field][0] === 'relativeHumidity') { const stateNameAirTemperature = [statePath, 'airTemperature'].join('.'); const stateNameStationPressure = [statePath, 'stationPressure'].join('.'); const stateNameAbsoluteHumidity = [statePath, 'absoluteHumidity'].join('.'); const stateParametersAbsoluteHumidity = { type: 'state', common: { type: 'number', unit: 'g/m³', read: true, write: false, role: 'value.humidity', name: 'absolute Humidity; adapter calculated', }, native: {}, }; const airTemp = await that.getValObj(stateNameAirTemperature); const stationPressure = await that.getValObj(stateNameStationPressure); if (airTemp !== null && stationPressure !== null) { // Calculate min/max for dewpoint that.calcMinMaxValue(stateNameAbsoluteHumidity, stateParametersAbsoluteHumidity, dewpoint(airTemp.val, fieldvalue, stationPressure.val).absoluteHumidity, MIN); that.calcMinMaxValue(stateNameAbsoluteHumidity, stateParametersAbsoluteHumidity, dewpoint(airTemp.val, fieldvalue, stationPressure.val).absoluteHumidity, MAX); that.myCreateState(stateNameAbsoluteHumidity, stateParametersAbsoluteHumidity, dewpoint(airTemp.val, fieldvalue, stationPressure.val).absoluteHumidity); } } // Feels like from temperature and humidity and wind //------------------------------------------------- // Is calculated and written when humidity is received (wind and temperature comes before that, so they should be current) if (messageInfo[item][field][0] === 'relativeHumidity') { const stateNameAirTemperature = [statePath, 'airTemperature'].join('.'); const stateNameFeelsLike = [statePath, 'feelsLike'].join('.'); const stateNameWindAvg = [statePath, 'windAvg'].join('.'); const stateParametersFeelsLike = { type: 'state', common: { type: 'number', unit: '°C', read: true, write: false, role: 'value.temperature.feelslike', name: 'Feels like temperature (Heat index/wind chill), °C; adapter calculated', }, native: {}, }; const obj1 = await that.getValObj(stateNameAirTemperature); const obj2 = await that.getValObj(stateNameWindAvg); if (obj1 !== null && obj2 !== null) { const airTemperature = obj1.val; const windAvg = obj2.val; // Calculate min/max for feelsLike that.calcMinMaxValue(stateNameFeelsLike, stateParametersFeelsLike, feelsLike(airTemperature, windAvg, fieldvalue), MIN); that.calcMinMaxValue(stateNameFeelsLike, stateParametersFeelsLike, feelsLike(airTemperature, windAvg, fieldvalue), MAX); that.myCreateState(stateNameFeelsLike, stateParametersFeelsLike, feelsLike(airTemperature, windAvg, fieldvalue)); } } // Convert wind directions from degrees to cardinal directions // ----------------------------------------------------------- if (messageInfo[item][field][0] === 'windDirection') { const stateNameWindDirectionText = [statePath, 'windDirectionCardinal'].join('.'); const stateParametersWindDirectionText = { type: 'state', common: { type: 'string', unit: '', read: true, write: false, role: 'text.direction.wind', name: 'Cardinal wind direction; adapter calculated', }, native: {}, }; that.myCreateState(stateNameWindDirectionText, stateParametersWindDirectionText, windDirections[Math.round(fieldvalue / 22.5)]); } // Convert wind speed from m/s to Beaufort // --------------------------------------- if (['windSpeed', 'windGust', 'windLull', 'windAvg'].includes(messageInfo[item][field][0])) { let stateNameBeaufort; switch (messageInfo[item][field][0]) { case 'windGust': stateNameBeaufort = [statePath, 'beaufortGust'].join('.'); break; case 'windLull': stateNameBeaufort = [statePath, 'beaufortLull'].join('.'); break; case 'windAvg': stateNameBeaufort = [statePath, 'beaufortAvg'].join('.'); break; default: stateNameBeaufort = [statePath, 'beaufort'].join('.'); } const stateParametersBeaufort = { type: 'state', common: { type: 'number', unit: '', read: true, write: false, role: 'value.speed.wind', name: 'Wind speed in Beaufort; adapter calculated', }, native: {}, }; // Calculate max for beaufort windspeeds that.calcMinMaxValue(stateNameBeaufort, stateParametersBeaufort, beaufort(fieldvalue), MAX); // Write new value to state (or create first, if needed) that.myCreateState(stateNameBeaufort, stateParametersBeaufort, beaufort(fieldvalue)); } // Sensor status as text from binary //--------------------------------- if (messageInfo[item][field][0] === 'sensor_status') { let sensorStatusText = ''; const stateNameSensorStatusText = [statePath, 'sensor_statusText'].join('.'); const stateParametersSensorStatusText = { type: 'state', common: { type: 'string', unit: '', read: true, write: false, role: 'text.status', name: 'Sensor status; adapted calculated', }, native: {}, }; Object.keys(sensorfails).forEach((item) => { if ((fieldvalue & parseInt(item)) === parseInt(item)) { if (sensorStatusText !== '') { sensorStatusText += ', '; } sensorStatusText += sensorfails[item]; } if (sensorStatusText === '') { sensorStatusText = 'Sensors OK'; } }); that.myCreateState(stateNameSensorStatusText, stateParametersSensorStatusText, sensorStatusText); } // Powermodes from sensor_status //--------------------------------- if (messageInfo[item][field][0] === 'sensor_status') { const stateNamePowerMode = [statePath, 'powerMode'].join('.'); const stateParametersPowerMode = { type: 'state', common: { type: 'mixed', states: { 0: 'Mode 0: Full power all sensors enabled', 1: 'Mode 1: Rapid sample interval set to six seconds', 2: 'Mode 2: Rapid sample interval set to one minute', 3: 'Mode 3: Rapid sample interval set to five minutes; Sensor sample interval set to five minutes; Lightning sensor disabled; Haptic sensor disabled', }, read: true, write: false, role: 'text.status', name: 'Power mode; adapter calculated', }, native: {}, }; let Mode = 0; Object.keys(powermodes).forEach((powermode) => { // eslint-disable-next-line no-bitwise if ((fieldvalue & parseInt(powermode)) === parseInt(powermode)) { Mode = powermodes[powermode]; } }); that.myCreateState(stateNamePowerMode, stateParametersPowerMode, Mode); } //= ============================= // End of special tasks section }); } }); } }); } /** * Is called when adapter shuts down - callback has to be called under any circumstances! * @param {() => void} callback */ onUnload(callback) { try { clearTimeout(timer); // stop timeout from loggin at stop mServer.close(); // close UDP port this.log.info('cleaned everything up...'); callback(); } catch (e) { callback(); } } /** * Write value to state or create if not already existing * @param {string} stateName The full path and name of the state to be created * @param {object} stateParameters Set of parameters for creation of state * @param {number | string | null | boolean} stateValue Value of the state (optional) * @param {number} expiry Time in seconds until the value is set back to false (optional) */ async myCreateState(stateName, stateParameters, stateValue = null, expiry = 0) { const that = this; if (!existingStates.includes(stateName)) { // state not existing? existingStates.push(stateName); // Remember state is existing or was created const obj = await this.setObjectNotExistsAsync(stateName, stateParameters); // create if not existing and log creation if (obj) { that.log.info(`Creating node: ${stateName}`); } } if (stateValue !== null) { // Write value if provided await this.setStateAsync(stateName, { val: stateValue, ack: true, expire: expiry }); } } /** * Return obj from state if key "val" exists, otherwise return null * @param {string} stateName THe full path and name of the state to be read */ async getValObj(stateName) { const obj = await this.getStateAsync(stateName); if (obj) { if ('val' in obj) { return obj; } } return null; } /** * Calculate min/max for today and yesterday * @param {string} stateName The full path and name of the state to be created * @param {object} stateParameters Set of parameters for creation of state * @param {number | string | null | boolean} stateValue Value of the state (optional) * @param {number} calcType 1='min' or 2='max' to calculate minimum or maximum value */ async calcMinMaxValue(stateName, stateParameters, stateValue, calcType) { const stateparts = stateName.split('.'); // split statename to insert min/max today/yesterday as levels const state = stateparts.pop(); // extract last as state const stateBase = [...stateparts].join('.'); // and put rest back together const minmaxStateParametersToday = JSON.parse(JSON.stringify(stateParameters)); // Make a real copy not just an addtl. reference const minmaxStateParametersYesterday = JSON.parse(JSON.stringify(stateParameters)); // Take parameters from main value ... let minmaxStateNameToday = ''; let minmaxStateNameYesterday = ''; switch (calcType) { case MIN: minmaxStateNameToday = `${stateBase}.today.min.${state}`; // create state name minmaxStateParametersToday.common.name += ' / today / min; adapter calculated'; // ... and add something to the name minmaxStateNameYesterday = `${stateBase}.yesterday.min.${state}`; // create state name minmaxStateParametersYesterday.common.name += ' / min / yesterday; adapter calculated'; // ... and add something to the name break; case MAX: minmaxStateNameToday = `${stateBase}.today.max.${state}`; // create state name minmaxStateParametersToday.common.name += ' / today / max; adapter calculated'; // ... and add something to the name minmaxStateNameYesterday = `${stateBase}.yesterday.max.${state}`; // create state name minmaxStateParametersYesterday.common.name += ' / yesterday / max; adapter calculated'; // ... and add something to the name break; default: } const obj = await this.getValObj(minmaxStateNameToday); // get old min/max value if (obj !== null) { if (now.getDay() === oldNow.getDay()) { // same day let newMinmaxValue; switch (calcType) { case MIN: newMinmaxValue = Math.min(obj.val, stateValue); // calculate new min value break; case MAX: newMinmaxValue = Math.max(obj.val, stateValue); // calculate new min value break; default: } if (obj.val !== newMinmaxValue) { // only update state if value is different this.myCreateState(minmaxStateNameToday, minmaxStateParametersToday, newMinmaxValue); // create and/or write node } } else { // new day, always update this.myCreateState(minmaxStateNameToday, minmaxStateParametersToday, stateValue); // On a new day, first value is the minimum of 'today' this.myCreateState(minmaxStateNameYesterday, minmaxStateParametersYesterday, obj.val); // Values for yesterday are last min value from today } } else { // min or max state does not yet exist, create this.myCreateState(minmaxStateNameToday, minmaxStateParametersToday, stateValue); // if not existing, create } } } // @ts-ignore parent is a valid property on module if (module.parent) { // Export the constructor in compact mode /** * @param {Partial<utils.AdapterOptions>} [options={}] */ module.exports = (options) => new WeatherflowUdp(options); } else { // otherwise start the instance directly new WeatherflowUdp(); } /** * QFF: Convert local absolute air pressure to seal level (DWD formula); http://dk0te.ba-ravensburg.de/cgi-bin/navi?m=WX_BAROMETER * @param {number} temperature The local air temperature in °C * @param {number} airPressureAbsolute The local station air pressure in hPa * @param {number} altitude The station altitude in m * @param {number} humidity The local air humidity * @returns {number} sea level reduced pressure */ function getQFF(temperature, airPressureAbsolute, altitude, humidity) { const g_n = 9.80665; // Erdbeschleunigung (m/s^2) const gam = 0.0065; // Temperaturabnahme in K pro geopotentiellen Metern (K/gpm) const R = 287.06; // Gaskonstante für trockene Luft (R = R_0 / M) // const M = 0.0289644; // Molare Masse trockener Luft (J/kgK) // const R_0 = 8.314472; // allgemeine Gaskonstante (J/molK) const T_0 = 273.15; // Umrechnung von °C in K const C = 0.11; // DWD-Beiwert für die Berücksichtigung der Luftfeuchte const E_0 = 6.11213; // (hPa) const f_rel = humidity / 100; // relative Luftfeuchte (0-1.0) // momentaner Stationsdampfdruck (hPa) const e_d = f_rel * E_0 * Math.exp((17.5043 * temperature) / (241.2 + temperature)); const reducedPressure = Math.round(10 * airPressureAbsolute * Math.exp((g_n * altitude) / (R * (temperature + T_0 + C * e_d + ((gam * altitude) / 2))))) / 10; return reducedPressure; } /** * Calculate dewpoint; Formula: https://www.wetterochs.de/wetter/feuchte.html * @param {number} temperature The local air temperature in °C * @param {number} humidity The local air humidity * @param {number | undefined} pressure [optional] the local station pressure * @returns {Object} */ function dewpoint(temperature, humidity, pressure = undefined) { // Konstanten const mw = 18.016; // Molekulargewicht des Wasserdampfes (kg/kmol) const gk = 8314.3; // universelle Gaskonstante (J/(kmol*K)) const t0 = 273.15; // Absolute Temperatur von 0 °C (Kelvin) let a; let b; if (temperature >= 0) { a = 7.5; b = 237.3; } else { a = 7.6; b = 240.7; } const SDD = 6.1078 * 10 ** ((a * temperature) / (b + temperature)); let DD = (humidity / 100) * SDD; if (pressure) { // if pressure is given, correct the DD DD = DD * pressure / 1013.25; } const v = Math.log(DD / 6.1078) / Math.log(10); const dewpointTemp = Math.round(((b * v) / (a - v)) * 10) / 10; // absolut humidity const absoluteHumidity = Math.pow(10, 5) * mw / gk * DD / (temperature + t0); return { dewpointTemp, absoluteHumidity }; } /** * Convert wind speed from m/s to beauforts * @param {number} windspeed Wind speed in m/s * @returns {number} Beaufort wind value */ function beaufort(windspeed) { let beaufortWind = 0; // max wind speeds m/s to Beaufort const beauforts = { 0: 0, 0.3: 1, 1.5: 2, 3.3: 3, 5.4: 4, 7.9: 5, 10.7: 6, 13.8: 7, 17.1: 8, 20.7: 9, 24.4: 10, 28.4: 11, 32.6: 12, }; Object.keys(beauforts).forEach((item) => { if (windspeed > parseFloat(item)) { beaufortWind = beauforts[item]; } }); return beaufortWind; } /** * Convert wind speed from m/s to beauforts * @param {number} temperature The local air temperature in °C * @param {number} windspeed The current wind speed in m/s * @param {number} humidity The local air humidity in % * @returns {number} Feels like temperature in °C */ function feelsLike(temperature, windspeed, humidity) { let feelsLikeTemperature; if (temperature >= 26.7 && humidity >= 40) { // heat index (https://de.wikipedia.org/wiki/Hitzeindex) feelsLikeTemperature = (-8.784695 + 1.61139411 * temperature + 2.338549 * humidity); feelsLikeTemperature += (-0.14611605 * temperature * humidit