UNPKG

signalk-sonoff-ewelink

Version:
1,107 lines (1,105 loc) 43.6 kB
"use strict"; /* * Copyright 2021 Scott Bender <scott@scottbender.net> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.default = default_1; const camelCase = require('camelcase'); const eWeLink = require('ewelink-api-next').default; const path = require('path'); const dnssd = require('dnssd2'); const fs = require('fs'); const crypto = require('crypto'); const nonce = (size = 8) => Math.random() .toString(36) .slice(-size); const fetch = require('node-fetch'); //the registered one const APP_ID = 'SsgewdgY5EHXEFIx6wAsmqf7dBE71c8i'; const APP_SECRET = 'oqqaAV5qMu2AWRqkl6Df2GltfkDxuKse'; //const APP_ID = 'unD0YlKmlbBtUGkaOQt0OywX7sGmzHat' //const APP_SECRET = 'NGMMshHMBCkWfy0Hwkiskw7iP07UHLGm' const pingTime = 25000; //const pingTime = 120000 //const pingTime = 10000 function default_1(app) { const error = app.error; const debug = app.debug; let sentMetaDevices = {}; let lanInfo; let lanClient; let client; let wsClient; let ws; let lanConnection; let cloudConnection; let anyCloudOnlyDevices; let cloudOnlyByDevice = {}; let putsRegistred = {}; let devicesCache; let families; let lanInfoPath; let deviceCachePath; let props; let wsTimer; let wsPingInterval; let pending = {}; let browser; const plugin = { start: function (properties) { props = properties; openConnection(); }, stop: function () { wsClient = undefined; client = undefined; if (ws) { ws.close(); ws = undefined; } if (wsTimer) { clearTimeout(wsTimer); wsTimer = undefined; } putsRegistred = {}; sentMetaDevices = {}; devicesCache = undefined; cloudOnlyByDevice = {}; }, id: 'signalk-sonoff-ewelink', name: 'Sonoff/eWeLink', description: 'Signal K Plugin For Sonoff/eWeLink devices', schema: () => { const schema = { type: 'object', required: ['authInfo'], properties: { authInfo: { type: 'string', lines: 10, title: 'Authentication Info', description: 'See plugin README' }, lanMode: { type: 'boolean', title: 'Use LAN Mode', default: true } } }; if (families) { schema.properties['family'] = { type: 'string', title: 'Family', enum: families.map((fam) => fam.id), enumNames: families.map((fam) => fam.name), default: families[0].id }; } if (devicesCache) { devicesCache.forEach((device) => { if (device.params && device.params.switches) { const devSchema = { type: 'object', properties: { deviceName: { type: 'string', title: 'Name', default: device.name, readOnly: true }, enabled: { type: 'boolean', title: 'Enabled', default: true }, bankPath: { type: 'string', title: 'Bank Path', default: camelCase(device.name), description: 'Used to generate the path name, ie. electrical.switches.${bankPath}.0.state' }, forceCloudMode: { type: 'boolean', title: 'Force Cloud Mode', default: false, description: 'Use the cloud for this device even if LAN mode is on' } } }; schema.properties[`Device ID ${device.deviceid}`] = devSchema; device.params.switches.forEach((sw) => { const name = device.tags?.ck_channel_name ? device.tags.ck_channel_name[sw.outlet.toString()] : sw.outlet.toString(); devSchema.properties[`Channel ${sw.outlet}`] = { type: 'object', title: `Channel ${sw.outlet + 1}`, properties: { displayName: { type: 'string', title: 'Display Name (meta)', default: name }, abbrev: { type: 'string', title: 'Abbreviated Name (meta)' }, enabled: { type: 'boolean', title: 'Enabled', default: true }, switchPath: { type: 'string', title: 'Switch Path', default: camelCase(name), description: 'Used to generate the path name, ie. electrical.switches.bank.${switchPath}.state' } } }; }); } else { schema.properties[`Device ID ${device.deviceid}`] = { type: 'object', properties: { deviceName: { type: 'string', title: 'Name', default: device.name, readOnly: true }, enabled: { type: 'boolean', title: 'Enabled', default: true }, displayName: { type: 'string', title: 'Display Name (meta)', default: device.name }, abbrev: { type: 'string', title: 'Abbreviated Name (meta)' }, switchPath: { type: 'string', title: 'Switch Path', default: camelCase(device.name), description: 'Used to generate the path name, ie electrical.switches.${switchPath}.state' }, forceCloudMode: { type: 'boolean', title: 'Force Cloud Mode', default: false, description: 'Use the cloud for this device even if LAN mode is on' } } }; if (device.extra.uiid == l1Light) { schema.properties[`Device ID ${device.deviceid}`].properties.includeDYIModes = { title: 'Include DYI Modes in Presets', type: 'boolean', default: false }; schema.properties[`Device ID ${device.deviceid}`].properties.presets = { title: 'Presets', type: 'array', items: { type: 'object', properties: { name: { type: 'string', title: 'Name' }, colorR: { type: 'number', title: 'Red', default: 255 }, colorG: { type: 'number', title: 'Green', default: 255 }, colorB: { type: 'number', title: 'Blue', default: 255 }, bright: { type: 'number', title: 'Brightness', description: 'Number between 1-100. Set to 0 to preserve current brightness', default: 100 } } } }; } } }); } return schema; }, uiSchema: () => { const uiSchema = { authInfo: { 'ui:widget': 'textarea' } }; if (devicesCache) { devicesCache.forEach((device) => { uiSchema[`Device ID ${device.deviceid}`] = { 'ui:field': 'collapsible', collapse: { field: 'ObjectField', wrapClassName: 'panel-group' } }; }); } return uiSchema; } }; async function openConnection() { /* let token = '5sdkflkfdfdkllk' const resp = await fetch(`https://us-apia.coolkit.cc/v2/family`, { headers: { "X-CK-Appid": APP_ID, Authorization: "Bearer " + token, 'Content-Type': 'application/json' } }) console.log(JSON.stringify(resp, null, 2)) const data = await resp.json(); console.log(JSON.stringify(data, null, 2)) */ deviceCachePath = path.join(app.getDataDirPath(), 'devices-cache-v2.json'); lanInfoPath = path.join(app.getDataDirPath(), 'lan-info.json'); if (props.authInfo === undefined) { app.setPluginError('Please enter your authentication info, See the Plugin README'); return; } const tokenInfo = JSON.parse(props.authInfo); const logging = { info: (msg) => { app.debug(msg); }, error: (msg) => { app.error(msg); } }; const config = { region: tokenInfo.region, appId: APP_ID, secert: APP_SECRET, appSecert: APP_SECRET, logObj: logging }; readDeviceCache(); client = new eWeLink.WebAPI(config); client.at = tokenInfo.accessToken; client.setUrl(tokenInfo.region); client.appSecret = APP_SECRET; const currentTime = new Date().getTime(); if (tokenInfo.atExpiredTime <= currentTime) { debug('token has expired, refreshing...'); const resp = await client.user.refreshToken({ rt: tokenInfo.refreshToken }); if (resp.error !== 0) { app.error(`error refreshing token: ${resp.msg}`); } else { debug('updating token info'); tokenInfo.accessToken = resp.data.at; tokenInfo.refreshToken = resp.data.rt; client.at = resp.data.at; if (resp.data.atExpiredTime) { tokenInfo.atExpiredTime = resp.data.atExpiredTime; } else { tokenInfo.atExpiredTime = currentTime + 24 * 60 * 60 * 1000 * 29; } if (resp.data.rtExpiredTime) { tokenInfo.rtExpiredTime = resp.data.rtExpiredTime; } else { tokenInfo.rtExpiredTime = currentTime + 24 * 60 * 60 * 1000 * 59; } props.authInfo = JSON.stringify(tokenInfo, null, 2); app.savePluginOptions(props, (err) => { if (err) { app.error(err); } }); } } let fam; let connectionFailed = false; try { fam = await client.home.getFamily({}); if (fam.error !== 0) { error(fam.msg); app.setPluginError(fam.msg); } else if (fam.data.familyList) { families = fam.data.familyList; } debug('families: %s', JSON.stringify(fam, null, 2)); let thingList = await client.device.getAllThingsAllPages({ familyId: props.family }); debug('things: %s', JSON.stringify(thingList, null, 2)); if (thingList?.error === 0) { devicesCache = thingList.data.thingList.map((x) => x.itemData); saveDeviceCache(); } else { error(thingList.msg); app.setPluginError(thingList.msg); } } catch (err) { error(err); app.setPluginError(err.message); connectionFailed = true; } if (devicesCache) { getDevices(devicesCache, true); devicesCache.forEach((device) => { const switchProps = getDeviceProps(device); let cloudOnly = (!switchProps || typeof switchProps.enabled === 'undefined' || switchProps.enabled) && (cloudOnlyHardware.indexOf(device.extra.uiid) !== -1 || switchProps?.forceCloudMode === true); cloudOnlyByDevice[device.deviceid] = cloudOnly; }); anyCloudOnlyDevices = devicesCache.find((device) => cloudOnlyByDevice[device.deviceid] === true); } let userApiKey; if (devicesCache.length > 0) { userApiKey = devicesCache[0].apikey; } if (!connectionFailed) { wsClient = new eWeLink.Ws({ ...config //logObj: console }); wsClient.userApiKey = userApiKey; wsClient.at = tokenInfo.accessToken; wsClient.region = tokenInfo.region; openWebSocket(); } if (props.lanMode) { readLanInfo(); if (!lanInfo) { lanInfo = {}; } lanClient = new eWeLink.Lan({ selfApikey: userApiKey, logObj: logging }); lanClient.discovery((server) => { debug('mdns server:', server); lanInfo[server.txt.id] = { ip: server.addresses[0], iv: server.txt.iv, port: server.port }; saveLanInfo(); //console.log(JSON.stringify(lanInfo, null, 2)) }); debug('starting dnsd browser...'); browser = dnssd.Browser(dnssd.tcp('ewelink')); browser .on('serviceChanged', dnsdChanged) .on('serviceUp', dnsdChanged) .start(); } } async function openWebSocket() { debug('opening websocket...'); ws = await wsClient.Connect.create({ appId: APP_ID, at: wsClient.at, region: wsClient.region, userApiKey: wsClient.userApiKey }, (ws) => { debug('websocket opened'); }, () => { ws = undefined; error('web socket closed'); if (wsClient) { wsTimer = setTimeout(() => { wsTimer = undefined; openWebSocket(); }, 5000); } }, (err) => { error(err); }, onMessage); } function onMessage(ws, event) { if (event.data === 'pong') { debug('got ws pong'); return; } let info = JSON.parse(event.data); debug('ws message from server: ' + JSON.stringify(info, null, 2)); if (info.action === 'update' || (info.error === 0 && info.deviceid && info.params)) { let device = getCachedDevice(info.deviceid); if (device) { const deviceProps = getDeviceProps(device); if (!deviceProps || typeof deviceProps === 'undefined' || deviceProps.enabled) { sendDeltas(device, info.params); } } else { debug(`unknown device: ${info.deviceid}`); } } else if (info.error !== undefined && info.sequence !== undefined) { const req = pending[info.sequence]; if (req) { delete pending[info.sequence]; const command = req.command; command.action = 'query'; command.nonce = nonce(); command.sequence = new Date().getTime().toString(); command.params = Object.keys(command.params); ws.send(JSON.stringify(command)); req.cb({ state: 'COMPLETED', statusCode: info.error === 0 ? 200 : 400, message: info.reason }); } } } function dnsdChanged(service) { const txt = service.txt || service.txtRecord; if (!txt || !txt.id || !txt.iv) { error('invalid mdns record'); error(JSON.stringify(service, null, 2)); } const deviceid = txt.id; const iv = txt.iv; debug('got dnsd for device %s (id:%s)', service.name, deviceid); const device = getCachedDevice(deviceid); if (!device) { return; } try { const info = lanClient.decrypt(txt.data1, crypto .createHash('md5') .update(device.devicekey) .digest('hex'), iv); sendDeltas(device, info); } catch (err) { if (!(err instanceof SyntaxError)) { error(err); } //app.setPluginError('unable to decrypt mdns data') } } /* function decryptMessage (msg: any, deviceKey: string, iv: any) { let encoded = '' for (let i = 1; i < 6; i++) { let part = msg[`data${i}`] if (part) { encoded = encoded + part } } return JSON.parse(decryptionData(encoded, deviceKey, iv)) } */ function propHandler(context, path, value, device, prop, cb, converter = null) { try { let params = converter ? converter(value) : { [prop]: value }; const update = wsClient.Connect.getUpdateState(device.deviceid, params); ws.send(update); const command = JSON.parse(update); pending[command.sequence] = { cb, command }; } catch (err) { error(err); app.setPluginError(err.message); cb({ state: 'COMPLETED', statusCode: 400, message: err.message }); } return { state: 'PENDING' }; } function registerPutProp(device, prop, converter = null) { const propPath = getSwitchPath(device, prop); app.registerPutHandler('vessels.self', propPath, (context, path, value, cb) => { return propHandler(context, path, value, device, prop, cb, converter); }); } function presetHandler(context, path, value, device, cb) { const switchProps = getSwitchProps(device); const preset = switchProps.presets.find((preset) => preset.name == value); if (value === 'Unknown' || !preset) { return { state: 'COMPLETED', statusCode: 400, message: `invalid value ${value}` }; } else { const params = { colorR: preset.colorR, colorG: preset.colorG, colorB: preset.colorB, switch: 'on' }; if (preset.bright !== 0) { params.bright = preset.bright; } try { const update = wsClient.Connect.getUpdateState(device.deviceid, params); ws.send(update); const command = JSON.parse(update); pending[command.sequence] = { cb, command }; } catch (err) { error(err); app.setPluginError(err.message); cb({ state: 'COMPLETED', statusCode: 400, message: err.message }); } return { state: 'PENDING' }; } } function filterEnabledDevices(devices) { return devices.filter((device) => { const deviceProps = getDeviceProps(device); return (!deviceProps || typeof deviceProps.enabled === 'undefined' || deviceProps.enabled); }); } function getDevices(devices, doSendDeltas) { filterEnabledDevices(devices).forEach((device) => { if (device.params && (device.params.switches || device.params.switch)) { if (device.params.switches) { device.params.switches.forEach((channel) => { const switchProps = getSwitchProps(device, channel.outlet); if (switchProps?.enabled) { const switchPath = getBankSwitchPath(device, channel.outlet); if (!putsRegistred[switchPath]) { app.registerPutHandler('vessels.self', switchPath, (context, path, value, cb) => { return bankHandler(context, path, value, device, channel.outlet, cb); }); putsRegistred[switchPath] = true; } } }); } else { const switchProps = getSwitchProps(device); if (switchProps?.enabled) { const switchPath = getSwitchPath(device); if (!putsRegistred[switchPath]) { app.registerPutHandler('vessels.self', switchPath, (context, path, value, cb) => { return switchHandler(context, path, value, device, cb); }); if (device.extra.uiid === l1Light) { registerPutProp(device, 'mode', (value) => { return { mode: Object.values(l1ModeMap).indexOf(value) + 1 }; }); registerPutProp(device, 'colorR'); registerPutProp(device, 'colorG'); registerPutProp(device, 'colorB'); registerPutProp(device, 'dimmingLevel', (value) => { let val = Number((value * 100).toFixed(0)); if (val === 0) { val = 1; } return { bright: val }; }); registerPutProp(device, 'speed'); registerPutProp(device, 'sensitive'); registerPutProp(device, 'light_type'); const switchProps = getSwitchProps(device); if (switchProps.presets) { app.registerPutHandler('vessels.self', getSwitchPath(device, 'preset'), (context, path, value, cb) => { return presetHandler(context, path, value, device, cb); }); } putsRegistred[switchPath] = true; } } } } if (doSendDeltas) { sendDeltas(device, device.params); } } }); } async function setBankPowerState(device, state, outlet, cb) { try { const stateStr = state ? 'on' : 'off'; const stateMap = { switches: [ { switch: stateStr, outlet } ] }; if (props.lanMode && !cloudOnlyByDevice[device.deviceid]) { const res = await sendZeroConf(device, stateMap); if (res.error !== 0) { return { status: 'error', message: res.message }; } setTimeout(() => { var val = app.getSelfPath(getBankSwitchPath(device, outlet)); if (val && val.value == (state ? 1 : 0)) { cb({ state: 'COMPLETED', statusCode: 200 }); } else { cb({ state: 'COMPLETED', statusCode: 400, message: 'Did not receive change confirmation' }); } }, 2000); return { status: 'PENDING' }; } else { const update = wsClient.Connect.getUpdateState(device.deviceid, stateMap); ws.send(update); return { status: 'PENDING', command: JSON.parse(update) }; } } catch (err) { return { status: 'error', message: err.message }; } } function bankHandler(context, path, value, device, outlet, cb) { const state = value === 1 || value === 'on' || value === 'true' || value === true; setBankPowerState(device, state, outlet, cb) .then((status) => { debug('got status outlet %d %j: ', outlet, status); if (status.status !== 'PENDING') { cb({ state: 'COMPLETED', statusCode: status.status === 'ok' ? 200 : 400, message: status.message }); } else if (pending.command) { pending[status.command.sequence] = { cb, command: status.command }; } }) .catch((err) => { error(err); app.setPluginError(err.message); cb({ state: 'COMPLETED', statusCode: 400, message: err.message }); }); return { state: 'PENDING' }; } async function sendZeroConf(device, data) { let info = getLanInfo(device.deviceid); if (!info) { return { error: 1, message: 'no lan info' }; } debug('sending zero config to %s: %j', device.deviceid, data); const func = data.switches ? lanClient.zeroconf.switches : lanClient.zeroconf.switch; return func({ ip: info.ip, port: info.port, data, deviceId: device.deviceid, secretKey: crypto .createHash('md5') .update(device.devicekey) .digest('hex'), encrypt: true, iv: info.iv }); } async function setPowerState(device, state, cb) { const stateStr = state ? 'on' : 'off'; try { if (props.lanMode && !cloudOnlyByDevice[device.deviceid]) { let info = getLanInfo(device.deviceid); if (!info) { return { status: 'error', message: 'no lan info' }; } const res = await sendZeroConf(device, { switch: stateStr }); if (res.error !== 0) { return { status: 'error', message: res.message }; } setTimeout(() => { var val = app.getSelfPath(getSwitchPath(device)); if (val && val.value == (state ? 1 : 0)) { cb({ state: 'COMPLETED' }); } else { cb({ state: 'COMPLETED', statusCode: 400, message: 'Did not receive change confirmation' }); } }, 2000); return { status: 'PENDING' }; } else { const update = wsClient.Connect.getUpdateState(device.deviceid, { switch: stateStr }); ws.send(update); const updateJ = JSON.parse(update); return { status: 'PENDING', command: updateJ }; } } catch (err) { return { status: 'error', message: err.message }; } } function switchHandler(context, path, value, device, cb) { const state = value === 1 || value === 'on' || value === 'true'; setPowerState(device, state, cb) .then((status) => { debug('set status: %j', status); if (status.status !== 'PENDING') { cb({ state: 'COMPLETED', statusCode: status.status === 'ok' ? 200 : 400, message: status.message }); } else if (status.command) { pending[status.command.sequence] = { cb, command: status.command }; } }) .catch((err) => { error(err); app.setPluginError(err.message); cb({ state: 'COMPLETED', statusCode: 400, message: err.message }); }); return { state: 'PENDING' }; } function sendMeta(device) { let meta = []; if (device.params.switches) { device.params.switches.forEach((channel) => { const switchProps = getSwitchProps(device, channel.outlet); if (!switchProps || typeof switchProps.enabled === 'undefined' || switchProps.enabled) { meta.push({ path: getBankSwitchPath(device, channel.outlet), value: { displayName: switchProps?.displayName || device.name, abbrev: switchProps?.abbrev, //units: 'bool', order: channel.outlet } }); meta.push({ path: getBankSwitchPath(device, channel.outlet, null), value: { displayName: switchProps?.displayName || device.name, abbrev: switchProps?.abbrev, order: channel.outlet } }); } }); } else if (device.params.switch) { const switchProps = getSwitchProps(device); if (!switchProps || typeof switchProps.enabled === 'undefined' || switchProps.enabled) { let extras = {}; if (device.extra.uiid === l1Light) { extras = { type: 'dimmer', canDimWhenOff: true }; meta.push({ path: getSwitchPath(device, 'mode'), value: { enum: Object.values(l1ModeMap), possibleValues: Object.values(l1ModeMap).map((mode) => { return { title: mode, value: mode }; }) } }); if (switchProps.presets) { meta.push({ path: getSwitchPath(device, 'preset'), value: { displayName: switchProps?.displayName || device.name, possibleValues: [ ...switchProps.presets.map((preset) => { return { title: preset.name, value: preset.name }; }) ], enum: [...switchProps.presets.map((preset) => preset.name)] } }); } } meta.push({ path: getSwitchPath(device), value: { ...extras, displayName: switchProps?.displayName || device.name, abbrev: switchProps?.abbrev //units: 'bool' } }); meta.push({ path: getSwitchPath(device, null), value: { ...extras, displayName: switchProps?.displayName || device.name, abbrev: switchProps?.abbrev } }); } } if (meta.length) { debug('sending meta: %j', meta); app.handleMessage(plugin.id, { updates: [ { meta } ] }); } } function sendDeltas(device, params) { let values; if (!sentMetaDevices[device.deviceid]) { sendMeta(device); sentMetaDevices[device.deviceid] = true; } device.params = { ...device.params, ...params }; if (params.switches) { values = params.switches .map((channel) => { const switchProps = getSwitchProps(device, channel.outlet); if (!switchProps || typeof switchProps.enabled === 'undefined' || switchProps.enabled) { return [ { path: getBankSwitchPath(device, channel.outlet), value: channel.switch === 'on' ? 1 : 0 }, { path: getBankSwitchPath(device, channel.outlet, 'order'), value: channel.outlet } ]; } else { return null; } }) .filter((kp) => kp != null); values = [].concat.apply([], values); } else { const switchProps = getSwitchProps(device); if (!switchProps || typeof switchProps.enabled === 'undefined' || switchProps.enabled) { values = []; if (params.switch) { values.push({ path: getSwitchPath(device), value: params.switch === 'on' ? 1 : 0 }); } let addValue = (key, v) => { const val = typeof v !== 'undefined' ? v : params[key]; if (typeof val !== 'undefined') { values.push({ path: getSwitchPath(device, key), value: val }); } }; if (device.extra.uiid === l1Light) { addValue('mode', l1ModeMap[params.mode]); addValue('colorR'); addValue('colorG'); addValue('colorB'); if (typeof params.bright !== 'undefined') { addValue('dimmingLevel', params.bright / 100.0); } addValue('speed'); addValue('sensitive'); addValue('light_type'); if (switchProps.presets) { const preset = switchProps.presets.find((preset) => { return (device.params.colorR == preset.colorR && device.params.colorG == preset.colorG && device.params.colorB == preset.colorB && (preset.bright === 0 || device.params.bright == preset.bright)); }); values.push({ path: getSwitchPath(device, 'preset'), value: preset?.name || 'Unknown' }); } } } } if (values) { let delta = { updates: [ { values } ] }; debug('sending delta %j', delta); app.handleMessage(plugin.id, delta); } } function getCachedDevice(deviceid) { return devicesCache.find((device) => device.deviceid == deviceid); } function getLanInfo(deviceid) { return lanInfo[deviceid]; } function readLanInfo() { try { const content = fs.readFileSync(lanInfoPath); lanInfo = JSON.parse(content); } catch (err) { error(err); } } function saveLanInfo() { try { fs.writeFileSync(lanInfoPath, JSON.stringify(lanInfo, null, 2)); } catch (err) { error(err); } } function readDeviceCache() { try { const content = fs.readFileSync(deviceCachePath); devicesCache = JSON.parse(content); } catch (err) { debug(err); } } function saveDeviceCache() { try { fs.writeFileSync(deviceCachePath, JSON.stringify(devicesCache, null, 2)); } catch (err) { error(err); } } function getDeviceProps(device) { return props[`Device ID ${device.deviceid}`] || {}; } function getSwitchProps(device, channel = undefined) { if (device.params.switches) { const bankConfig = props[`Device ID ${device.deviceid}`] || {}; return bankConfig[`Channel ${channel}`]; } else { return props[`Device ID ${device.deviceid}`] || {}; } } function getSwitchPath(device, key = 'state') { const config = props[`Device ID ${device.deviceid}`] || {}; return `electrical.switches.${config.switchPath || camelCase(device.name)}${key ? '.' + key : ''}`; } function getBankSwitchPath(device, channel, key = 'state') { const bankConfig = props[`Device ID ${device.deviceid}`] || {}; let path = bankConfig[`Channel ${channel}`]?.switchPath; let cloud = device.tags?.ck_channel_name ? device.tags?.ck_channel_name[channel.toString()] : undefined; if (!path && cloud) { path = camelCase(cloud); } return `electrical.switches.${bankConfig.bankPath || camelCase(device.name)}.${path || channel}${key ? '.' + key : ''}`; } return plugin; } const l1Light = 59; const cloudOnlyHardware = [l1Light]; const l1ModeMap = { 1: 'Colorful', 2: 'Colorful Gradient', 3: 'Colorful Breath', 4: 'DIY Gradient', 5: 'DIY Pulse', 6: 'DIY Breath', 7: 'DIY Strobe', 8: 'RGB Gradient', 9: 'RGB Pulse', 10: 'RGB Breath', 11: 'RBG Strobe', 12: 'Sync to music' }; //# sourceMappingURL=index.js.map