UNPKG

iobroker.shelly

Version:
1,016 lines (877 loc) 44 kB
'use strict'; const datapoints = require('../datapoints'); const axios = require('axios').default; const crypto = require('node:crypto'); function isAsync(funct) { if (funct && funct.constructor) { return funct.constructor.name === 'AsyncFunction'; } return undefined; } class BaseClient { constructor(type, adapter, objectHelper, eventEmitter) { this.type = type; // mqtt or coap this.adapter = adapter; this.objectHelper = objectHelper; this.eventEmitter = eventEmitter; this.httpIoBrokerStateTimeout = null; this.msgId = 1; this.stateValueCache = {}; this.device = {}; this.http = {}; this.deviceId; // e.g. SHBDUO-1#8CAAB5616291#2 (used in object IDs) this.ip; // e.g. 192.168.1.2 this.id; // e.g. ShellyBulbDuo-8CAAB5616291 this.deviceType; // e.g. SHBDUO-1 or SHRGBW2 this.deviceMode; // e.g. color or white / relay or roller this.deviceClass; // e.g. ShellyBulbDuo or shellyrgbw2 this.serialId; // e.g. 8CAAB5616291 this.deviceGen; // 1 or 2 this.nonceCount = 0; this.httpTimeout = 8 * 1000; // Handle firmware updates this.eventEmitter.on('onFirmwareUpdate', async () => await this.firmwareUpdate()); // Handle script download this.eventEmitter.on('onScriptDownload', async () => await this.downloadAllScripts()); } async requestAsync(url) { const httpDebugDir = `httpDebug/${this.getDirectoryName()}`; if (this.adapter.config.saveHttpResponses) { await this.adapter.mkdirAsync(this.adapter.namespace, 'httpDebug'); await this.adapter.mkdirAsync(this.adapter.namespace, httpDebugDir); } return new Promise((resolve, reject) => { if (!this.getIP()) { reject( `[requestAsync] Unable to perform HTTP request "${url}" - IP address is unknown of ${this.getLogInfo()}`, ); } const httpDebugFilePath = `${httpDebugDir}/${url.replace(/[^a-zA-Z0-9-_.]/g, '_')}.json`; let axiosRequestObj = { method: 'get', responseType: 'text', transformResponse: res => { return res; // Avoid automatic json parse }, baseURL: `http://${this.getIP()}`, timeout: this.httpTimeout, url, }; if (this.getDeviceGen() === 1) { if (this.adapter.config.httpusername && this.adapter.config.httppassword) { // Add basic auth if configured this.adapter.log.silly( `[requestAsync] HTTP request to gen 1 device with basic auth: "${axiosRequestObj.baseURL}${url}"`, ); axiosRequestObj = { ...axiosRequestObj, auth: { username: this.adapter.config.httpusername, password: this.adapter.config.httppassword, }, }; } else { this.adapter.log.silly( `[requestAsync] HTTP request to gen 1 device: "${axiosRequestObj.baseURL}${url}"`, ); } axios(axiosRequestObj) .then(response => { this.adapter.log.silly( `[requestAsync] HTTP response of gen 1 device: "${axiosRequestObj.baseURL}${url}" -> "${response.data}"`, ); if (this.adapter.config.saveHttpResponses) { this.adapter.log.silly(`[requestAsync] Saving HTTP debug file to ${httpDebugFilePath}`); this.adapter.writeFile(this.adapter.namespace, httpDebugFilePath, response.data); } resolve(response.data); }) .catch(reject); } else if (this.getDeviceGen() >= 2) { this.adapter.log.silly( `[requestAsync] HTTP request to Gen 2+ device (with digest auth): "${axiosRequestObj.baseURL}${url}"`, ); axios(axiosRequestObj) .then(response => { this.adapter.log.silly( `[requestAsync] HTTP response of Gen 2+ device without auth: "${axiosRequestObj.baseURL}${url}" -> "${response.data}"`, ); if (this.adapter.config.saveHttpResponses) { this.adapter.log.silly(`[requestAsync] Saving HTTP debug file to ${httpDebugFilePath}`); try { const responseObj = JSON.parse(response.data); this.adapter.writeFile( this.adapter.namespace, httpDebugFilePath, JSON.stringify(responseObj, null, 2), ); } catch { this.adapter.writeFile(this.adapter.namespace, httpDebugFilePath, response.data); } } resolve(response.data); }) .catch(err => { if ( err && err.response && err.response?.status === 401 && err.response?.headers?.['www-authenticate'] ) { const authDetails = err.response.headers['www-authenticate'] .replace('Digest ', '') .split(', ') .map(v => { const l = v.split('='); return { [l[0]]: l[1].replace(/"/g, '') }; }) .reduce((prev, curr) => ({ ...prev, ...curr }), {}); const username = 'admin'; const password = this.adapter.config.httppassword; this.nonceCount++; const nonceCount = `00000000${this.nonceCount}`.slice(-8); const cnonce = crypto.randomBytes(24).toString('hex'); const realm = authDetails.realm; const nonce = authDetails.nonce; const sha256 = str => crypto.createHash('sha256').update(str).digest('hex'); const HA1 = sha256(`${username}:${realm}:${password}`); const HA2 = sha256(`GET:${url}`); const response = sha256(`${HA1}:${nonce}:${nonceCount}:${cnonce}:auth:${HA2}`); const authorization = `Digest username="${username}", realm="${realm}", nonce="${nonce}", uri="${url}", cnonce="${cnonce}", nc=${nonceCount}, qop=auth, response="${response}", algorithm=SHA-256`; axiosRequestObj = { ...axiosRequestObj, headers: { Authorization: authorization, }, }; axios(axiosRequestObj) .then(response => { this.adapter.log.silly( `[requestAsync] HTTP response of Gen 2+ device with digest auth: "${axiosRequestObj.baseURL}${url}" -> "${response.data}"`, ); if (this.adapter.config.saveHttpResponses) { this.adapter.log.silly( `[requestAsync] Saving HTTP debug file to ${httpDebugFilePath}`, ); try { const responseObj = JSON.parse(response.data); this.adapter.writeFile( this.adapter.namespace, httpDebugFilePath, JSON.stringify(responseObj, null, 2), ); } catch { this.adapter.writeFile( this.adapter.namespace, httpDebugFilePath, response.data, ); } } resolve(response.data); }) .catch(reject); } else { reject(err); } }) .catch(reject); } }); } getNextMsgId() { return this.msgId++ & 0xffff; } /** * Returns the polltime for a device (in seconds) */ getPolltime() { if (this.adapter.config.polltime === 0) { return 0; } const deviceClass = this.getDeviceClass(); let polltime = undefined; if (deviceClass) { polltime = datapoints.getPolltime(deviceClass); if (polltime === undefined) { polltime = this.adapter.config.polltime || 0; } if (this.adapter.config.polltime > polltime) { polltime = this.adapter.config.polltime; } return polltime; } return this.adapter.config.polltime; // Default = instance config } /** * ID of the Shelly device * Example: shellyplug-s-12345 * * @returns */ getId() { return this.id; } /** * Serial id of the Shelly device * Example: shellyplug-s-12345 */ getSerialId() { throw new Error('You have to implement the method getSerialId!'); } /** * @returns */ getDeviceClass() { throw new Error('You have to implement the method getDeviceClass!'); } /** * @returns */ getDeviceType() { throw new Error('You have to implement the method getDeviceType!'); } publishStateValue(cmd, value) { throw new Error(`You have to implement the method publishStateValue! ${cmd} ${value}`); } getDirectoryName() { return this.getId().replace(/[^a-zA-Z0-9-_.]/g, '_'); } getDeviceMode() { return this.deviceMode; } async setDeviceMode(newDeviceMode) { if (newDeviceMode !== this.deviceMode) { if (this.deviceMode) { this.adapter.log.info( `Device mode of ${this.getLogInfo()} changed from "${this.deviceMode}" to "${newDeviceMode}" - recreating states NOW!`, ); } this.deviceMode = newDeviceMode; await this.adapter.setState(`${this.getDeviceId()}.Sys.deviceMode`, { val: newDeviceMode, ack: true }); await this.deleteOldStates(); await this.createObjects(); } } async initDeviceModeFromState() { if (!this.deviceMode) { const deviceModeState = await this.adapter.getStateAsync(`${this.getDeviceId()}.Sys.deviceMode`); if (deviceModeState && deviceModeState?.val) { this.deviceMode = deviceModeState.val; this.adapter.log.debug( `Configured device mode of ${this.getLogInfo()} by state "${this.getDeviceId()}.Sys.deviceMode" (fallback): ${this.deviceMode}`, ); } } } /** * IP of Shelly device * Example 192.168.1.2 * * @returns */ getIP() { return this.ip; } async setIP(ip, source) { if (!ip) { return; } this.ip = ip; this.adapter.log.debug(`[setIP] New IP for device ${this.getDeviceId()}: ${ip} (source: ${source})`); const hostNameObj = await this.adapter.getObjectAsync(`${this.getDeviceId()}.hostname`); if (hostNameObj) { await this.adapter.setState(`${this.getDeviceId()}.hostname`, { val: ip, ack: true, c: source }); } } async initIPFromState() { if (!this.ip) { const ipState = await this.adapter.getStateAsync(`${this.getDeviceId()}.hostname`); if (ipState && ipState?.val) { this.ip = ipState.val; this.adapter.log.debug( `Configured IP of ${this.getLogInfo()} by state "${this.getDeviceId()}.hostname" (fallback): ${this.ip}`, ); } } } /** * Returns a string for logging with the IP address, name of device and type * * @returns */ getLogInfo() { return `${this.getIP() ?? ''} (${this.getDeviceClass()} / ${this.getId()} / ${this.getDeviceId()})`.trim(); } /** * Change Device ID from #2 to #1 (firmware version >= 1.8) * Example SHRGBW2#1234#2 will change to SHRGBW2#1234#1 * * @param deviceId */ getOldDeviceInfo(deviceId) { if (deviceId && deviceId.substr(-2) === '#2') { return `${deviceId.substr(0, deviceId.length - 2)}#1`; } return deviceId; } /** * Get the Shelly Device type with the serial number of the device back. * Device type could be for example SHRGBW2. The serial number of the * device like 1234 will be added * Example: SHRGBW2#1234#1 */ getDeviceId() { return this.deviceId ? this.getOldDeviceInfo(this.deviceId) : undefined; } /** * Shelly device configuration exists * If missing: Add the configuration in the ./lib/devices directory (see docs) */ deviceExists() { return datapoints.getDeviceTypeByClass(this.getDeviceClass()) ? true : false; } /** * Checks if Shelly supports rpc api * you have to add a configuration in the ./lib/devices directory */ getDeviceGen() { if (this.deviceGen === undefined) { const deviceClass = this.getDeviceClass(); this.deviceGen = datapoints.getDeviceGen(deviceClass); } return this.deviceGen; } /** * Returns the url to Shelly knowledge base */ getKnowledgeBaseUrl() { return datapoints.getKnowledgeBaseUrlByClass(this.getDeviceClass()); } isOnline() { return this.adapter.isOnline(this.getDeviceId()); } getAllTypePublishStates() { const states = []; for (const i in this.device) { const state = this.device[i]; if (this.type === 'mqtt') { if (state?.mqtt?.mqtt_publish) { states.push(state); } } else if (this.type === 'coap') { if (state?.coap?.coap_publish) { states.push(state); } } } return states; } /** * Missing data will be pulled by HTTP */ async httpIoBrokerState() { if (!this.isOnline() || !this.getIP()) { this.adapter.log.silly( `[httpIoBrokerState] Device ${this.getLogInfo()} is offline (or IP is unknown) - waiting`, ); this.httpIoBrokerStateTimeout = setTimeout( async () => await this.httpIoBrokerState(), this.adapter.config.polltime * 1000, ); return; } this.adapter.log.silly(`[httpIoBrokerState] Running for ${this.getLogInfo()}: ${JSON.stringify(this.http)}`); let polltime = this.getPolltime(); for (const httpUrl in this.http) { const dps = this.http[httpUrl]; try { const body = await this.requestAsync(httpUrl); for (const i in dps) { const dp = this.device[dps[i]]; if (dp && dp.state) { const fullStateId = `${this.getDeviceId()}.${dp.state}`; let value = body; try { this.adapter.log.silly( `[httpIoBrokerState] Updating state ${fullStateId} for ${this.getLogInfo()}: ${body}`, ); if (dp[this.type] && dp[this.type].http_publish_funct) { value = isAsync(dp[this.type].http_publish_funct) ? await dp[this.type].http_publish_funct(value, this) : dp[this.type].http_publish_funct(value, this); } if (dp.common.type === 'boolean' && value === 'false') { value = false; } if (dp.common.type === 'boolean' && value === 'true') { value = true; } if (dp.common.type === 'number' && value !== undefined && value !== null) { value = Number(value); } if (Object.prototype.hasOwnProperty.call(this.device, dp.state)) { if ( value !== undefined && (!Object.prototype.hasOwnProperty.call(this.stateValueCache, fullStateId) || this.stateValueCache[fullStateId] !== value || this.adapter.config.updateUnchangedObjects) ) { this.adapter.log.debug( `[httpIoBrokerState] State change ${this.getLogInfo()}: state: ${fullStateId}, value: ${JSON.stringify(value)}`, ); this.stateValueCache[fullStateId] = value; this.objectHelper.setOrUpdateObject( fullStateId, { type: 'state', common: dp.common, }, ['name'], value, ); } } } catch (err) { if (err.name && err.name.startsWith('TypeError')) { this.adapter.log.debug( `[httpIoBrokerState] Could not find property for state ${fullStateId} for ${this.getLogInfo()} "${httpUrl}": ${err}`, ); } else { this.adapter.log.error( `[httpIoBrokerState] Error for state ${fullStateId} for ${this.getLogInfo()} "${httpUrl}": ${err} - value: "${value}"`, ); } } } } this.objectHelper.processObjectQueue(() => {}); } catch (err) { // Increase polltime on http error if (polltime > 0 && polltime < 60) { polltime = 60; } if (err && err.response && err.response.status == 401) { this.adapter.log.error( `[httpIoBrokerState] HTTP request error for ${this.getLogInfo()} "${httpUrl}": Wrong http username or http password! Please enter user credentials for restricted login.`, ); } else if (err && err.response && err.response.status == 404) { this.adapter.log.debug( `[httpIoBrokerState] HTTP request error for ${this.getLogInfo()} "${httpUrl}": 404 Not Found - this can happen if the current device configuration doesn't support this feature.`, ); } else { this.adapter.log.debug( `[httpIoBrokerState] HTTP request error for ${this.getLogInfo()} "${httpUrl}": ${err}`, ); } } } if (this.http && Object.keys(this.http).length > 0 && polltime > 0) { this.httpIoBrokerStateTimeout = setTimeout(async () => await this.httpIoBrokerState(), polltime * 1000); } } /** * Create objects in shelly.X. for a new Shelly device * The Shelly device must exist in the ./lib/devices/gen<X>/ directory */ async createObjects() { return new Promise((resolve, reject) => { try { this.adapter.log.debug( `[createObjects] Starting object creation of ${this.getLogInfo()} for mode: ${this.getDeviceMode() ?? '<not set>'}`, ); const deviceStatesHttp = {}; const deviceStates = datapoints.getDeviceByClass( this.getDeviceClass(), this.type, this.getDeviceMode(), ); if (deviceStates) { for (const stateId in deviceStates) { const state = deviceStates[stateId]; state.state = stateId; let hasTypeCmd = false; let hasTypePublishCmd = false; if (this.type === 'coap') { hasTypeCmd = !!state?.coap?.coap_cmd; hasTypePublishCmd = !!state?.coap?.coap_publish; } if (this.type === 'mqtt') { hasTypeCmd = !!state.mqtt?.mqtt_cmd; hasTypePublishCmd = !!state?.mqtt?.mqtt_publish; } const deviceId = this.getDeviceId(); this.objectHelper.setOrUpdateObject( deviceId, { type: 'device', common: { name: `Device ${deviceId}`, desc: `Gen ${this.getDeviceGen()}`, statusStates: { onlineId: `${this.adapter.namespace}.${deviceId}.online`, }, }, native: {}, }, ['name'], ); const channel = stateId.split('.').slice(0, 1).join(); if (channel !== stateId) { const channelId = `${deviceId}.${channel}`; this.objectHelper.setOrUpdateObject( channelId, { type: 'channel', common: { name: `Channel ${channel}`, }, }, ['name'], ); } const fullStateId = `${deviceId}.${stateId}`; let controlFunction; // HTTP if (state[this.type] && state[this.type].http_cmd && !hasTypeCmd) { this.adapter.log.silly( `[http controlFunction] Found ${this.type}.http_cmd of state ${fullStateId} for ${this.getLogInfo()}`, ); controlFunction = async value => { if (this.isOnline()) { let publishValue = value; this.adapter.log.silly( `[http controlFunction] Entered ${this.type}.http_cmd of state ${fullStateId} for ${this.getLogInfo()} with value: ${value}`, ); // Modify value with http_cmd_funct (if exists) if (state[this.type]?.http_cmd_funct) { try { publishValue = isAsync(state[this.type].http_cmd_funct) ? await state[this.type].http_cmd_funct(value, this) : state[this.type].http_cmd_funct(value, this); this.adapter.log.debug( `[http controlFunction] Executing state.${this.type}.http_cmd_funct of state ${fullStateId} for ${this.getLogInfo()} with value: ${value} -> ${JSON.stringify(publishValue)}`, ); } catch (err) { this.adapter.log.error( `[http controlFunction] Error in function state.${this.type}.http_cmd_funct of state ${fullStateId} for ${this.getLogInfo()}: ${err}`, ); } } let body; try { if (this.getDeviceGen() === 1) { // Append value parameters const url = new URL(`http://${this.getIP()}${state[this.type].http_cmd}`); if (typeof publishValue === 'object') { Object.keys(publishValue).forEach(key => url.searchParams.append(key, publishValue[key]), ); } this.adapter.log.debug( `[http controlFunction] Executing gen 1 state.${this.type}.http_cmd of state ${fullStateId} for ${this.getLogInfo()} from url: ${url.href}`, ); body = await this.requestAsync(url.pathname + url.search); if (body === 'Bad default state and button type combination!') { this.adapter.log.error( `[http controlFunction] Error in function state.${this.type}.http_cmd of state ${fullStateId} for ${this.getLogInfo()}: ${body}`, ); } } else if (this.getDeviceGen() >= 2) { // Not supported this.adapter.log.error( `[http controlFunction] Error in function state.${this.type}.http_cmd of state ${fullStateId} for ${this.getLogInfo()}: Not supported for Gen 2+ devices (${this.getDeviceGen()})`, ); } } catch (err) { if (err && err.response && err.response.status == 401) { this.adapter.log.error( `[http controlFunction] Error in function state.${this.type}.http_cmd of state ${fullStateId} for ${this.getLogInfo()}: Wrong http username or http password! Please enter user credentials for restricted login.`, ); } else { this.adapter.log.error( `[http controlFunction] Error in function state.${this.type}.http_cmd of state ${fullStateId} for ${this.getLogInfo()}: ${err}`, ); } } if (value == this.stateValueCache?.[fullStateId]) { this.adapter.log.debug( `[http controlFunction] Ack state.${this.type}.http_cmd of state ${fullStateId} for ${this.getLogInfo()} in adapter: Value not changed`, ); await this.adapter.setState(fullStateId, { val: value, ack: true, c: `UNCHANGED_AUTO_ACK`, }); } else { // Remove from cache, since value changed delete this.stateValueCache[fullStateId]; } } else { this.adapter.log.warn( `[http controlFunction] Unable to perform request - device ${this.getLogInfo()} is offline`, ); } }; } // MQTT if (this.type === 'mqtt' && state[this.type] && hasTypeCmd) { this.adapter.log.silly( `[mqtt controlFunction] Found ${this.type}.mqtt_cmd of state ${fullStateId} for ${this.getLogInfo()}`, ); controlFunction = async value => { if (this.isOnline()) { let publishValue = value; this.adapter.log.silly( `[mqtt controlFunction] Entered ${this.type}.mqtt_cmd of state ${fullStateId} for ${this.getLogInfo()} with value: ${value}`, ); // Modify value with mqtt_cmd_funct (if exists) if (state[this.type]?.mqtt_cmd_funct) { try { publishValue = isAsync(state[this.type].mqtt_cmd_funct) ? await state[this.type].mqtt_cmd_funct(value, this) : state[this.type].mqtt_cmd_funct(value, this); this.adapter.log.debug( `[mqtt controlFunction] Executing state.${this.type}.mqtt_cmd_funct of state ${fullStateId} for ${this.getLogInfo()} with value: ${value} -> ${publishValue}`, ); } catch (err) { this.adapter.log.error( `[mqtt controlFunction] Error in function state.${this.type}.mqtt_cmd_funct of state ${fullStateId} for ${this.getLogInfo()}: ${err}`, ); } } if (this.publishStateValue !== undefined) { this.publishStateValue(state[this.type].mqtt_cmd, publishValue); } if (value == this.stateValueCache?.[fullStateId]) { this.adapter.log.debug( `[mqtt controlFunction] Ack state.${this.type}.mqtt_cmd of state ${fullStateId} for ${this.getLogInfo()} in adapter: Value not changed`, ); await this.adapter.setState(fullStateId, { val: value, ack: true, c: `UNCHANGED_AUTO_ACK`, }); } else { // Remove from cache, since value changed delete this.stateValueCache[fullStateId]; } } else { this.adapter.log.warn( `[mqtt controlFunction] Unable to perform request - device ${this.getLogInfo()} is offline`, ); } }; } // Fill this.http object for httpIoBrokerState() if (state?.[this.type]?.http_publish && !hasTypePublishCmd) { const key = state[this.type].http_publish; if (!deviceStatesHttp[key]) { deviceStatesHttp[key] = [stateId]; } else { deviceStatesHttp[key].push(stateId); } } // Init value or funct let value = undefined; if (state?.[this.type]?.init_value) { value = state?.[this.type]?.init_value; } else if (state?.[this.type]?.init_funct) { value = state[this.type].init_funct(this); } this.objectHelper.setOrUpdateObject( fullStateId, { type: 'state', common: state.common, }, ['name'], value, controlFunction, false, ); } } this.objectHelper.processObjectQueue(() => { this.http = deviceStatesHttp; this.device = deviceStates; this.adapter.log.debug(`[createObjects] Finished object creation of ${this.getLogInfo()}`); resolve(true); }); } catch (err) { reject(err); } }); } /** * delete old states in objects unter shelly.X. * For example if the configuration for the device change */ async deleteOldStates() { const deviceId = this.getDeviceId(); const objList = await this.adapter.getAdapterObjectsAsync(); const dps = datapoints.getDeviceByClass(this.getDeviceClass(), this.type, this.getDeviceMode()); if (dps) { for (const o in objList) { const tmpObj = objList[o]; if (tmpObj && tmpObj._id && tmpObj.type) { // remove namespace const stateId = tmpObj._id.replace(`${this.adapter.namespace}.${deviceId}.`, ''); // Just delete states of this device! if (tmpObj.type === 'state' && tmpObj._id.startsWith(`${this.adapter.namespace}.${deviceId}`)) { if (!dps[stateId]) { try { if (this.objectHelper.getObject(tmpObj._id)) { this.objectHelper.deleteObject(tmpObj._id); } else { await this.adapter.delForeignObjectAsync(tmpObj._id); } delete objList[tmpObj._id]; delete this.device[stateId]; this.adapter.log.debug(`Deleted unused state "${tmpObj._id}"`); } catch (err) { this.adapter.log.error(`Could not delete unused state "${tmpObj._id}": ${err}`); } } } } } } // Delete empty channels for (const o in objList) { const tmpObj = objList[o]; if (tmpObj && tmpObj.type && tmpObj._id && tmpObj.type === 'channel') { // Search for states in current channel let found = false; for (const j in objList) { const tmpidj = objList[j]; if (!tmpidj) { continue; } if ( tmpidj && tmpidj.type && tmpidj._id && tmpidj.type === 'state' && tmpidj._id.startsWith(tmpObj._id) ) { found = true; break; } } if (found === false) { try { if (this.objectHelper.getObject(tmpObj._id)) { this.objectHelper.deleteObject(tmpObj._id); } else { await this.adapter.delForeignObjectAsync(tmpObj._id, { recursive: true }); } delete objList[tmpObj._id]; this.adapter.log.debug(`Deleted unused channel "${tmpObj._id}"`); } catch (err) { this.adapter.log.error(`Could not delete unused channel "${tmpObj._id}": ${err}`); } } } } } async firmwareUpdate() { if (this.getDeviceId() === undefined) { return; } if (!this.isOnline()) { this.adapter.log.debug(`[firmwareUpdate] Device is offline ${this.getLogInfo()}`); return; } try { const firmwareUpdateState = await this.adapter.getStateAsync(`${this.getDeviceId()}.firmware`); this.adapter.log.silly( `[firmwareUpdate] received state ${JSON.stringify(firmwareUpdateState)} for ${this.getLogInfo()}`, ); // Check if device has firmware update if (firmwareUpdateState && firmwareUpdateState.val) { this.adapter.log.debug( `[firmwareUpdate] Update available for ${this.getLogInfo()} - performing update process`, ); if (this.getDeviceGen() === 1) { const body = await this.requestAsync('/ota?update=true'); this.adapter.log.debug(`[firmwareUpdate] Update result for ${this.getLogInfo()}: ${body}`); } else if (this.getDeviceGen() >= 2) { const body = await this.requestAsync('/rpc/Shelly.Update'); this.adapter.log.debug(`[firmwareUpdate] Update result for ${this.getLogInfo()}: ${body}`); } } } catch (err) { this.adapter.log.error(`[firmwareUpdate] Error in update request for ${this.getLogInfo()}: ${err}`); } } async downloadAllScripts() { if (this.getDeviceId() === undefined) { return; } if (!this.isOnline()) { this.adapter.log.debug(`[downloadAllScripts] Device is offline ${this.getLogInfo()}`); return; } try { if (this.getDeviceGen() >= 2) { const scriptDir = `scripts/${this.getDirectoryName()}`; await this.adapter.mkdirAsync(this.adapter.namespace, scriptDir); const body = await this.requestAsync('/rpc/Script.List'); this.adapter.log.debug(`[downloadAllScripts] Found scripts on device ${this.getLogInfo()}: ${body}`); const scriptList = JSON.parse(body); let downloadCounter = 0; for (const script of scriptList.scripts) { if (script.id && script.name) { const bodyCode = await this.requestAsync(`/rpc/Script.GetCode?id=${script.id}`); const scriptCode = JSON.parse(bodyCode); await this.adapter.writeFileAsync( this.adapter.namespace, `${scriptDir}/${script.id}-${script.name}.js`, scriptCode.data, ); downloadCounter++; } } this.adapter.log.info( `[downloadAllScripts] Downloaded ${downloadCounter} scripts of ${this.getLogInfo()} to files`, ); } } catch (err) { this.adapter.log.error(`[downloadAllScripts] Error ${this.getLogInfo()}: ${err}`); } } destroy() { this.adapter.log.debug(`[BaseClient] Destroying ${this.getLogInfo()}`); this.adapter.deviceStatusUpdate(this.getDeviceId(), false); // Device offline if (this.httpIoBrokerStateTimeout) { clearTimeout(this.httpIoBrokerStateTimeout); } this.msgId = 1; this.stateValueCache = {}; this.device = {}; this.http = {}; this.deviceId = undefined; this.ip = undefined; this.id = undefined; this.deviceType = undefined; this.deviceMode = undefined; this.deviceClass = undefined; this.serialId = undefined; this.deviceGen = undefined; } } class BaseServer { constructor(adapter, objectHelper, eventEmitter) { this.adapter = adapter; this.objectHelper = objectHelper; this.eventEmitter = eventEmitter; } destroy() { this.adapter.log.debug(`[BaseServer] Destroying`); } } module.exports = { BaseClient, BaseServer, };