UNPKG

iobroker.sainlogic

Version:

Read data from a sainlogic based weather station

393 lines (350 loc) 13.8 kB
/* jshint node: true */ 'use strict'; /* * Created with @iobroker/create-adapter v1.24.1 */ // The adapter-core module gives you access to the core ioBroker functions // you need to create an adapter const utils = require('@iobroker/adapter-core'); //const util = require('node:util'); const Parser = require('expr-eval').Parser; // Load your modules here, e.g.: const Listener = require('./lib/listener'); const Scheduler = require('./lib/scheduler'); const { DATAFIELDS } = require('./lib/constants'); //const getMethods = (obj) => Object.getOwnPropertyNames(obj).filter(item => typeof obj[item] === 'function'); class Sainlogic extends utils.Adapter { /** * @param [options] Adapter options */ constructor(options) { super({ ...options, name: 'sainlogic', }); this.on('ready', this.onReady.bind(this)); //this.on('objectChange', this.onObjectChange.bind(this)); this.on('stateChange', this.onStateChange.bind(this)); // this.on('message', this.onMessage.bind(this)); this.on('unload', this.onUnload.bind(this)); } checkUnit(attrdef, obj) { const c_id = obj._id; const target_unit = this.config[attrdef.unit_config]; if (target_unit != obj.common.unit) { // change and convert unit this.log.info(`Unit changed for ${c_id} from ${obj.common.unit} to ${target_unit}, updating data point`); this.extendObject(c_id, { type: obj.type, common: { name: attrdef.display_name, type: attrdef.type, role: attrdef.role, unit: target_unit, }, native: {}, }); const my_source_unit = attrdef.units.filter(function (unit) { return unit.display_name == obj.common.unit; }); const conversion_rule_back = my_source_unit[0].main_unit_conversion; this.getState(c_id, (err, st) => { const parser = new Parser(); if (st) { let new_value = st.val; // convert back if required if (conversion_rule_back != null) { const exp = parser.parse(conversion_rule_back); new_value = exp.evaluate({ x: new_value }); } new_value = this.toDisplayUnit(attrdef, new_value); this.setState(c_id, { val: new_value, ack: true }); } }); } } /** * Converts the given value to the target unit for given attribute definition * * @param attrdef Attribute definition * @param value Value to convert * @returns converted value */ toDisplayUnit(attrdef, value) { const parser = new Parser(); const target_unit = this.config[attrdef.unit_config]; const my_target_unit = attrdef.units.filter(function (unit) { return unit.display_name == target_unit; }); if (!my_target_unit) { this.log.warn( `No unit definition found for ${attrdef.id} and target unit '${target_unit}', using raw value`, ); return value; } const conversion_rule_forward = my_target_unit[0].display_conversion; this.log.debug( `Target for ${attrdef.id} unit is set: ${target_unit}, using conversion: ${conversion_rule_forward}`, ); let new_value = value; // convert forward if required if (conversion_rule_forward != null) { const exp = parser.parse(conversion_rule_forward); new_value = exp.evaluate({ x: value }); } return new_value; } async onStateChange(id, state) { if (state) { // The state was changed this.log.info(`state ${id} changed: ${state.val} (ack = ${state.ack})`); // react to user changes (ack === false) on our choicelist if (id && state.ack === false) { try { this.handleChoiceChange(id, state); } catch (e) { this.log.error(`Error handling choicelist change: ${e}`); } } } else { // The state was deleted this.log.info(`state ${id} deleted`); } } /** * Is called when databases are connected and adapter received configuration. */ async onReady() { // Initialize your adapter here // The adapters config (in the instance object everything under the attribute "native") is accessible via // this.config: if (this.config.scheduler_active == true) { this.log.info('Starting Scheduler'); this.scheduler = new Scheduler(this.config, this); this.scheduler.start(); } if (this.config.listener_active == true) { this.listener = new Listener( this.config.listener_ip, this.config.listener_port, this.config.path, this.config.listener_protocol, this.config.listener_forward_url, this, ); this.listener.start(); } } /** * Handle user changes to the choicelist state. * Only called for user-initiated changes (ack === false). * * @param {string} id full state id * @param {{val: any, ack: boolean}} state state object */ handleChoiceChange(id, state) { this.log.info(`User changed state (${id}) -> ${state.val}`); // Execute the action associated with this choice // TODO add flexible action handling here // this.executeChoiceAction(state.val, state); // Acknowledge the state so the adapter shows the value as processed this.setState(id, { val: state.val, ack: true }); } /** * Perform the action for a given choice value. * Replace or extend the switch body with the desired behavior. * * @param {string|number} value selected choice value * @param {{val: any, ack: boolean}} _state state object (unused) */ executeChoiceAction(value, _state) { switch (String(value)) { case '0': this.log.info('Choice action: Off selected'); // TODO: implement Off behavior break; case '1': this.log.info('Choice action: On selected'); // TODO: implement On behavior break; case '2': this.log.info('Choice action: Auto selected'); // TODO: implement Auto behavior break; default: this.log.warn(`Unknown choice value: ${value}`); } } verify_datapoint(obj_id, that, attrdef, attrname, value) { // check target type and type-cast if needed let default_value = ''; if (attrdef.type == 'number') { if (value != null) { default_value = 0; value = parseFloat(value); } else { value = 0; default_value = 0; } } else if (attrdef.type == 'string') { value = `${value}`; } this.getObject( obj_id, function (err, obj) { if (err || obj == null) { that.log.info(`Creating new data point: ${obj_id}`); that.setObjectNotExists( obj_id, { type: 'state', common: { name: attrname, type: attrdef.type, unit: attrdef.unit, role: attrdef.role, min: attrdef.min, max: attrdef.max, states: attrdef.states, def: default_value, read: true, write: attrdef.writeable ? true : false, mobile: { admin: { visible: true, }, }, }, native: {}, }, function (err, obj) { // now update the value if (err || obj == null) { that.log.error(`Error creating object ${obj_id}: ${err}`); } else { that.setStateAsync(obj_id, { val: value, ack: true }); } }, ); // add listener for relevant state changes if (attrdef.subscribe) { that.subscribeStates(obj_id); } } else { if (attrdef.unit_config != null) { that.checkUnit(attrdef, obj); } // now update the value that.setStateAsync(obj_id, { val: value, ack: true }); } }.bind(that), ); } /** * @param {Date} date Date of the update * @param obj_values JSON response with values */ setStates(date, obj_values) { this.setStateAsync('info.last_update', { val: date.toString(), ack: true }); for (const attr in obj_values) { // extract attribute id w/o channel const c_id = attr.split('.').pop(); // check if this has a mapping to the current protocol this.log.debug(`Setting value from data for ${attr} to ${obj_values[attr]}`); const my_attr_def = DATAFIELDS.filter(function (def) { return def.id == c_id; }); let display_val = obj_values[attr]; if (my_attr_def[0].unit_config) { display_val = this.toDisplayUnit(my_attr_def[0], display_val); } this.verify_datapoint(attr, this, my_attr_def[0], my_attr_def[0].channels[0].name, display_val); // allways channel 0 as primary attribute name if (c_id == 'winddir') { const winddir_attrdef = DATAFIELDS.filter(function (def) { return def.id == 'windheading'; }); this.verify_datapoint( 'weather.current.windheading', this, winddir_attrdef[0], winddir_attrdef[0].channels[0].name, this.getHeading(display_val, 16), ); } } } // taken from https://www.programmieraufgaben.ch/aufgabe/windrichtung-bestimmen/ibbn2e7d getHeading(degrees, precision) { this.log.debug(`Determining wind heading for ${degrees} and precision ${precision}`); precision = precision || 16; let directions = [], direction = 0; const step = 360 / precision; let i = step / 2; switch (precision) { case 4: directions = 'N O S W'.split(' '); break; case 8: directions = 'N NO O SO S SW W NW'.split(' '); break; case 16: directions = ('N NNO NO ONO O OSO SO ' + 'SSO S SSW SW WSW W WNW NW NNW').split(' '); break; case 32: directions = ( 'N NzO NNO NOzN NO NOzO ONO OzN O OzS OSO ' + 'SOzO SO SOzS SSO SzO S SzW SSW SWzS SW SWzW WSW WzS W WzN ' + 'WNW NWzW NW NWzN NNW NzW' ).split(' '); break; default: this.log.error('Wind heading could not be determined: invalid precision'); return ''; } if (degrees < 0 || degrees > 360) { this.log.error('Wind heading could not be determined: wind direction outside boundary'); return ''; } if (degrees <= i || degrees >= 360 - i) { return 'N'; } while (i <= degrees) { direction++; i += step; } this.log.debug(`GetHeading returning: ${directions[direction]}`); return directions[direction]; } /** * Is called when adapter shuts down - callback has to be called under any circumstances! * * @param {() => void} callback Callback function */ onUnload(callback) { try { if (this.listener) { this.listener.stop(); } if (this.scheduler) { this.scheduler.stop(); } this.log.info('Sainlogic Adapter gracefully shut down...'); callback(); } catch (e) { this.log.error(`Error during shutdown of Sainlogic Adapter: ${e}`); callback(); } } } // @ts-expect-error parent is a valid property on module if (module.parent) { // Export the constructor in compact mode /** * @param {Partial<ioBroker.AdapterOptions>} [options] Adapter options */ module.exports = options => new Sainlogic(options); } else { // otherwise start the instance directly new Sainlogic(); }