@homebridge-plugins/homebridge-govee 11.32.2-beta.2 → 11.32.2-beta.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/config.schema.json +1 -1
  3. package/lib/connection/ble.js +26 -1
  4. package/lib/connection/openapi.js +18 -0
  5. package/lib/device/appliance-basic.js +377 -0
  6. package/lib/device/dehumidifier-H7150.js +9 -3
  7. package/lib/device/diffuser-H7161.js +7 -1
  8. package/lib/device/fan-H1370.js +7 -1
  9. package/lib/device/fan-H7100.js +36 -4
  10. package/lib/device/fan-H7105.js +7 -1
  11. package/lib/device/fan-H7107.js +7 -1
  12. package/lib/device/heater1a.js +52 -12
  13. package/lib/device/heater1b.js +14 -3
  14. package/lib/device/heater2.js +7 -1
  15. package/lib/device/humidifier-H7140.js +7 -1
  16. package/lib/device/humidifier-H7141.js +7 -1
  17. package/lib/device/humidifier-H7142.js +7 -1
  18. package/lib/device/humidifier-H7143.js +7 -1
  19. package/lib/device/humidifier-H7160.js +7 -1
  20. package/lib/device/ice-maker-H7172.js +19 -7
  21. package/lib/device/index.js +44 -51
  22. package/lib/device/kettle.js +7 -1
  23. package/lib/device/outlet-single.js +7 -1
  24. package/lib/device/purifier-H7120.js +14 -25
  25. package/lib/device/purifier-H7122.js +7 -1
  26. package/lib/device/purifier-H7123.js +9 -3
  27. package/lib/device/purifier-H7126.js +7 -1
  28. package/lib/device/{sensor-button.js → sensor-basic.js} +14 -2
  29. package/lib/device/sensor-presence.js +7 -1
  30. package/lib/device/sensor-thermo4.js +13 -2
  31. package/lib/device/switch-double.js +34 -15
  32. package/lib/device/switch-triple.js +36 -17
  33. package/lib/device/template.js +15 -2
  34. package/lib/platform.js +83 -799
  35. package/lib/utils/capabilities.js +220 -0
  36. package/lib/utils/command-builder.js +6 -4
  37. package/lib/utils/config-parser.js +280 -0
  38. package/lib/utils/constants.js +120 -8
  39. package/lib/utils/decode.js +4 -1
  40. package/lib/utils/device-capabilities.js +77 -3
  41. package/lib/utils/device-context.js +179 -0
  42. package/lib/utils/device-merge.js +251 -0
  43. package/lib/utils/dispatch.js +159 -0
  44. package/lib/utils/functions.js +9 -0
  45. package/lib/utils/lang-en.js +0 -2
  46. package/lib/utils/report-unknown.js +156 -0
  47. package/package.json +1 -1
  48. package/lib/device/dehumidifier-H7151.js +0 -189
  49. package/lib/device/dehumidifier-H7152.js +0 -180
  50. package/lib/device/diffuser-H7162.js +0 -123
  51. package/lib/device/fan-H1310.js +0 -284
  52. package/lib/device/fan-H7101.js +0 -340
  53. package/lib/device/fan-H7102.js +0 -284
  54. package/lib/device/fan-H7106.js +0 -284
  55. package/lib/device/fan-H7111.js +0 -345
  56. package/lib/device/humidifier-H7145.js +0 -187
  57. package/lib/device/humidifier-H7147.js +0 -187
  58. package/lib/device/humidifier-H7148.js +0 -187
  59. package/lib/device/humidifier-H7149.js +0 -187
  60. package/lib/device/humidifier-H714E.js +0 -187
  61. package/lib/device/ice-maker-H717D.js +0 -218
  62. package/lib/device/ice-maker-H8120.js +0 -218
  63. package/lib/device/outlet-double.js +0 -188
  64. package/lib/device/outlet-triple.js +0 -160
  65. package/lib/device/purifier-H7121.js +0 -406
  66. package/lib/device/purifier-H7124.js +0 -504
  67. package/lib/device/purifier-H7127.js +0 -359
  68. package/lib/device/purifier-H7128.js +0 -359
  69. package/lib/device/purifier-H7129.js +0 -359
  70. package/lib/device/purifier-H712C.js +0 -359
  71. package/lib/device/sensor-contact.js +0 -22
  72. package/lib/utils/ble-timeouts.test.js +0 -44
  73. package/lib/utils/functions.test.js +0 -292
@@ -0,0 +1,220 @@
1
+ /**
2
+ * What a device can do, worked out from several sources rather than one table.
3
+ *
4
+ * Writing out all six hundred models by hand is a job nobody finishes, and it
5
+ * throws away the two things we already have: Govee's own API says what each
6
+ * device can do, and some behaviour depends on firmware rather than model. So
7
+ * instead of a table this asks a series of layers and takes the first answer:
8
+ *
9
+ * 1. user config an explicit setting always wins
10
+ * 2. firmware this model, at or above this firmware version
11
+ * 3. model written down because someone worked it out
12
+ * 4. discovered what this particular device reported it can do
13
+ * 5. family what this kind of appliance usually has
14
+ *
15
+ * Most models never need an entry of their own. They are answered by what the
16
+ * device reported plus the family defaults, which is why this scales to models
17
+ * nobody has written down.
18
+ *
19
+ * ⚠️ Family defaults are deliberately generous. A control this plugin has
20
+ * offered for years may be in use by someone, and taking it away to tidy up
21
+ * would break a working setup. Only a model entry - written after someone
22
+ * confirmed it - is allowed to say a device does not have something.
23
+ */
24
+
25
+ import { compareVersions } from './device-capabilities.js'
26
+
27
+ /**
28
+ * What each kind of appliance usually offers.
29
+ *
30
+ * These are the fallback, so they describe the group rather than any one model.
31
+ * Where a group has historically been given a control, it stays here even if
32
+ * some members turn out not to have it - see the warning above.
33
+ */
34
+ const familyControls = {
35
+ heater1: { swing: true, lock: true, light: false },
36
+ heater2: { swing: true, lock: true, light: true },
37
+ fan: { swing: true, lock: false, light: false },
38
+ humidifier: { swing: false, lock: false, light: false },
39
+ dehumidifier: { swing: false, lock: false, light: false },
40
+ purifier: { swing: false, lock: false, light: false },
41
+ iceMaker: { swing: false, lock: false, light: false },
42
+ diffuser: { swing: false, lock: false, light: false },
43
+ }
44
+
45
+ const emptyControls = {
46
+ swing: false,
47
+ lock: false,
48
+ light: false,
49
+ humiditySensor: false,
50
+ temperatureSensor: false,
51
+ modes: false,
52
+ }
53
+
54
+ /**
55
+ * Controls worked out for a particular model.
56
+ *
57
+ * Only add a model here once its controls are actually known. An entry is
58
+ * trusted over what the device reports, and it is the only thing allowed to
59
+ * take a control away.
60
+ */
61
+ const modelControls = {
62
+ // Heaters. These match what Govee's own app offers for each model.
63
+ // Added new, so there is nobody whose setup could be broken by them
64
+ H7137: { swing: false, lock: false, light: false },
65
+ H713E: { swing: false, lock: false, light: false },
66
+
67
+ // Diffusers are power only, which is what the plugin already offers
68
+ H7161: { swing: false, lock: false, light: false },
69
+ H7162: { swing: false, lock: false, light: false },
70
+ }
71
+
72
+ /**
73
+ * Controls a model is believed NOT to have, which are still being offered.
74
+ *
75
+ * These are recorded rather than applied. Each has been offered for a long
76
+ * time, so an owner may be relying on it, and taking it away to match a
77
+ * capability list would break a working setup for the sake of tidiness. The
78
+ * evidence is written down here so nobody has to work it out twice, and so
79
+ * there is a list to check the next time an owner of one of these gets in
80
+ * touch.
81
+ *
82
+ * Move an entry into `modelControls` once an owner confirms the control really
83
+ * does nothing.
84
+ */
85
+ const unconfirmedMissing = {
86
+ H713A: { swing: false },
87
+ H713B: { swing: false },
88
+ H713C: { swing: false },
89
+ H7135: { swing: false, light: false },
90
+ }
91
+
92
+ /**
93
+ * Controls that only appear at or above a firmware version. Same shape as the
94
+ * protocol overrides next door: a list per model, oldest rule first.
95
+ */
96
+ const firmwareControls = {}
97
+
98
+ /**
99
+ * What Govee's API calls a capability, and which control it means to us.
100
+ *
101
+ * These names come back per device, so they say what this particular unit can
102
+ * do rather than what its model generally can. That makes them the only source
103
+ * here that works for a model nobody has written down.
104
+ */
105
+ const discoveryMap = {
106
+ oscillationToggle: 'swing',
107
+ lockToggle: 'lock',
108
+ nightlightToggle: 'light',
109
+ mainLightToggle: 'light',
110
+ backgroundLightToggle: 'light',
111
+ sensorHumidity: 'humiditySensor',
112
+ sensorTemperature: 'temperatureSensor',
113
+ workMode: 'modes',
114
+ }
115
+
116
+ /**
117
+ * Turn the capability list a device reported into controls.
118
+ *
119
+ * Only ever adds. A capability the device did not mention is left undecided so
120
+ * a later layer can answer, because the API not listing something is not proof
121
+ * the device cannot do it.
122
+ */
123
+ function fromDiscovery(openApiCapabilities) {
124
+ const discovered = {}
125
+ Object.keys(openApiCapabilities || {}).forEach((instance) => {
126
+ const control = discoveryMap[instance]
127
+ if (control) {
128
+ discovered[control] = true
129
+ }
130
+ })
131
+ return discovered
132
+ }
133
+
134
+ /**
135
+ * Config settings that name a control directly, so an owner can turn something
136
+ * off that their device does not really have without waiting for a release.
137
+ */
138
+ function fromConfig(deviceConf) {
139
+ const chosen = {}
140
+ if (deviceConf?.hideLight === true) {
141
+ chosen.light = false
142
+ }
143
+ if (deviceConf?.hideSwing === true) {
144
+ chosen.swing = false
145
+ }
146
+ return chosen
147
+ }
148
+
149
+ function firmwareLayer(model, firmware) {
150
+ if (!firmware) {
151
+ return {}
152
+ }
153
+ const applied = {}
154
+ const rules = [...(firmwareControls[model] || [])]
155
+ .sort((a, b) => compareVersions(a.minVersion, b.minVersion))
156
+ rules.forEach((rule) => {
157
+ if (compareVersions(firmware, rule.minVersion) >= 0) {
158
+ Object.assign(applied, rule.controls)
159
+ }
160
+ })
161
+ return applied
162
+ }
163
+
164
+ /**
165
+ * Work out what a device can do.
166
+ *
167
+ * @param {object} context describing the device
168
+ * @param {string} context.model the govee model, eg 'H7130'
169
+ * @param {string} [context.family] which set of defaults to fall back on
170
+ * @param {string} [context.firmware] the firmware version the device reported
171
+ * @param {object} [context.openApiCapabilities] what the api said it can do
172
+ * @param {object} [context.deviceConf] this device's settings
173
+ * @returns {{ controls: object, sources: object }} the controls, and which
174
+ * layer decided each one - the second is for the log, so an unexpected answer
175
+ * can be traced without guessing
176
+ */
177
+ export function resolveControls(context = {}) {
178
+ const {
179
+ model,
180
+ family,
181
+ firmware,
182
+ openApiCapabilities,
183
+ deviceConf,
184
+ } = context
185
+
186
+ // Lowest priority first, so a later layer overwrites an earlier one
187
+ const layers = [
188
+ ['family', familyControls[family] || {}],
189
+ ['discovered', fromDiscovery(openApiCapabilities)],
190
+ ['model', modelControls[model] || {}],
191
+ ['firmware', firmwareLayer(model, firmware)],
192
+ ['config', fromConfig(deviceConf)],
193
+ ]
194
+
195
+ const controls = { ...emptyControls }
196
+ const sources = {}
197
+
198
+ layers.forEach(([name, values]) => {
199
+ Object.entries(values).forEach(([control, value]) => {
200
+ controls[control] = value
201
+ sources[control] = name
202
+ })
203
+ })
204
+
205
+ return { controls, sources }
206
+ }
207
+
208
+ /**
209
+ * A one-line summary of where each answer came from, for the debug log. Reading
210
+ * "swing from family" tells you nobody has confirmed it, which is a different
211
+ * situation from "swing from model".
212
+ */
213
+ export function describeSources(sources) {
214
+ return Object.entries(sources)
215
+ .sort(([a], [b]) => a.localeCompare(b))
216
+ .map(([control, source]) => `${control} from ${source}`)
217
+ .join(', ') || 'nothing known'
218
+ }
219
+
220
+ export { familyControls, modelControls, unconfirmedMissing }
@@ -67,7 +67,7 @@ function buildStateDualCommand(params) {
67
67
  }
68
68
 
69
69
  function buildStateOutletCommand(params, context) {
70
- const caps = getDeviceCapabilities(context.gvModel)
70
+ const caps = getDeviceCapabilities(context.gvModel, context.firmware)
71
71
  return {
72
72
  awsParams: {
73
73
  cmd: 'turn',
@@ -118,7 +118,7 @@ function buildOpenApiCommand(params) {
118
118
  }
119
119
 
120
120
  function buildBrightnessCommand(params, context) {
121
- const caps = getDeviceCapabilities(context.gvModel)
121
+ const caps = getDeviceCapabilities(context.gvModel, context.firmware)
122
122
  return {
123
123
  awsParams: {
124
124
  cmd: 'brightness',
@@ -143,7 +143,7 @@ function buildBrightnessCommand(params, context) {
143
143
  }
144
144
 
145
145
  function buildColorCommand(params, context) {
146
- const caps = getDeviceCapabilities(context.gvModel)
146
+ const caps = getDeviceCapabilities(context.gvModel, context.firmware)
147
147
  const { r, g, b } = params.value
148
148
 
149
149
  let awsParams
@@ -180,7 +180,7 @@ function buildColorCommand(params, context) {
180
180
  }
181
181
 
182
182
  function buildColorTempCommand(params, context) {
183
- const caps = getDeviceCapabilities(context.gvModel)
183
+ const caps = getDeviceCapabilities(context.gvModel, context.firmware)
184
184
  const [r, g, b] = k2rgb(params.value)
185
185
 
186
186
  let awsParams
@@ -224,6 +224,8 @@ function buildSceneCommand(params) {
224
224
  if (params.value[0]) {
225
225
  const splitCode = params.value[0].split(',')
226
226
  result.awsParams = { cmd: 'ptReal', data: { command: splitCode } }
227
+ // The local api accepts `ptReal` even though it is not in the documented
228
+ // command set, which is what lets scenes work without the cloud
227
229
  result.lanParams = { cmd: 'ptReal', data: { command: splitCode } }
228
230
  }
229
231
  if (params.value[1]) {
@@ -0,0 +1,280 @@
1
+ import platformConsts from './constants.js'
2
+ import { parseDeviceId } from './functions.js'
3
+ import platformLang from './lang-en.js'
4
+
5
+ const WHITESPACE_REGEX = /\s+/g
6
+
7
+ /**
8
+ * Reads the user's settings, warns about anything that will not be used, and
9
+ * fills in the defaults for the rest.
10
+ *
11
+ * Lifted out of the platform unchanged. It was the largest single thing in
12
+ * there and had nothing to do with talking to devices, so a change to how a
13
+ * device works no longer means opening the same file as a change to how a
14
+ * setting is read.
15
+ *
16
+ * It writes `config`, `deviceConf` and `ignoredDevices` onto the platform
17
+ * rather than returning them, which is how it always worked.
18
+ */
19
+ export function applyUserConfig(platform, config) {
20
+ // These shorthand functions save line space during config parsing
21
+ const logDefault = (k, def) => {
22
+ platform.log.warn('%s [%s] %s %s.', platformLang.cfgItem, k, platformLang.cfgDef, def)
23
+ }
24
+ const logDuplicate = (k) => {
25
+ platform.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgDup)
26
+ }
27
+ const logIgnore = (k) => {
28
+ platform.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgIgn)
29
+ }
30
+ const logIgnoreItem = (k) => {
31
+ platform.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgIgnItem)
32
+ }
33
+ const logIncrease = (k, min) => {
34
+ platform.log.warn('%s [%s] %s %s.', platformLang.cfgItem, k, platformLang.cfgLow, min)
35
+ }
36
+ const logQuotes = (k) => {
37
+ platform.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgQts)
38
+ }
39
+ const logRemove = (k) => {
40
+ platform.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgRmv)
41
+ }
42
+
43
+ // Begin applying the user's config
44
+ Object.entries(config).forEach((entry) => {
45
+ const [key, val] = entry
46
+ switch (key) {
47
+ case 'apiKey':
48
+ case 'code':
49
+ case 'password':
50
+ case 'username':
51
+ if (typeof val !== 'string' || val === '') {
52
+ logIgnore(key)
53
+ } else {
54
+ platform.config[key] = val
55
+ }
56
+ break
57
+ case 'lanScanSubnets': {
58
+ // Comma-separated list of CIDR subnets to scan directly (for devices
59
+ // on a different subnet from Homebridge, which multicast can't reach)
60
+ if (typeof val !== 'string' || val.trim() === '') {
61
+ break
62
+ }
63
+ const subnets = val
64
+ .split(',')
65
+ .map(s => s.trim())
66
+ .filter(s => /^\d{1,3}(?:\.\d{1,3}){3}\/\d{1,2}$/.test(s))
67
+ if (subnets.length === 0) {
68
+ logIgnore(key)
69
+ } else {
70
+ platform.config[key] = subnets
71
+ }
72
+ break
73
+ }
74
+ case 'awsDisable':
75
+ case 'bleDisable':
76
+ case 'colourSafeMode':
77
+ case 'disableDeviceLogging':
78
+ case 'ignoreMatter':
79
+ case 'lanDisable':
80
+ if (typeof val === 'string') {
81
+ logQuotes(key)
82
+ }
83
+ platform.config[key] = val === 'false' ? false : !!val
84
+ break
85
+ case 'bleControlInterval':
86
+ case 'bleRefreshTime':
87
+ case 'httpRefreshTime':
88
+ case 'lanRefreshTime':
89
+ case 'lanScanInterval': {
90
+ if (typeof val === 'string') {
91
+ logQuotes(key)
92
+ }
93
+ const intVal = Number.parseInt(val, 10)
94
+ if (Number.isNaN(intVal)) {
95
+ logDefault(key, platformConsts.defaultValues[key])
96
+ platform.config[key] = platformConsts.defaultValues[key]
97
+ } else if (intVal < platformConsts.minValues[key]) {
98
+ logIncrease(key, platformConsts.minValues[key])
99
+ platform.config[key] = platformConsts.minValues[key]
100
+ } else {
101
+ platform.config[key] = intVal
102
+ }
103
+ break
104
+ }
105
+ case 'dehumidifierDevices':
106
+ case 'fanDevices':
107
+ case 'heaterDevices':
108
+ case 'humidifierDevices':
109
+ case 'iceMakerDevices':
110
+ case 'kettleDevices':
111
+ case 'leakDevices':
112
+ case 'lightDevices':
113
+ case 'purifierDevices':
114
+ case 'diffuserDevices':
115
+ case 'switchDevices':
116
+ case 'thermoDevices':
117
+ if (Array.isArray(val) && val.length > 0) {
118
+ val.forEach((x) => {
119
+ if (!x.deviceId) {
120
+ logIgnoreItem(key)
121
+ return
122
+ }
123
+ const id = parseDeviceId(x.deviceId)
124
+ if (Object.keys(platform.deviceConf).includes(id)) {
125
+ logDuplicate(`${key}.${id}`)
126
+ return
127
+ }
128
+ const entries = Object.entries(x)
129
+ if (entries.length === 1) {
130
+ logRemove(`${key}.${id}`)
131
+ return
132
+ }
133
+ platform.deviceConf[id] = {}
134
+ entries.forEach((subEntry) => {
135
+ const [k, v] = subEntry
136
+ switch (k) {
137
+ case 'adaptiveLightingShift':
138
+ case 'brightnessStep':
139
+ case 'lowBattThreshold': {
140
+ if (typeof v === 'string') {
141
+ logQuotes(`${key}.${k}`)
142
+ }
143
+ const intVal = Number.parseInt(v, 10)
144
+ if (Number.isNaN(intVal)) {
145
+ logDefault(`${key}.${id}.${k}`, platformConsts.defaultValues[k])
146
+ platform.deviceConf[id][k] = platformConsts.defaultValues[k]
147
+ } else if (intVal < platformConsts.minValues[k]) {
148
+ logIncrease(`${key}.${id}.${k}`, platformConsts.minValues[k])
149
+ platform.deviceConf[id][k] = platformConsts.minValues[k]
150
+ } else {
151
+ platform.deviceConf[id][k] = intVal
152
+ }
153
+ break
154
+ }
155
+ case 'awsBrightnessNoScale':
156
+ case 'hideLight':
157
+ case 'hideModeGreenTea':
158
+ case 'hideModeOolongTea':
159
+ case 'hideModeCoffee':
160
+ case 'hideModeBlackTea':
161
+ case 'showCustomMode1':
162
+ case 'showCustomMode2':
163
+ case 'showExtraSwitch':
164
+ case 'tempReporting':
165
+ if (typeof v === 'string') {
166
+ logQuotes(`${key}.${id}.${k}`)
167
+ }
168
+ platform.deviceConf[id][k] = v === 'false' ? false : !!v
169
+ break
170
+ case 'awsColourMode':
171
+ case 'openApiTempUnit':
172
+ case 'showAs': {
173
+ if (typeof v !== 'string' || !platformConsts.allowed[k].includes(v)) {
174
+ logIgnore(`${key}.${id}.${k}`)
175
+ } else {
176
+ platform.deviceConf[id][k] = v
177
+ }
178
+ break
179
+ }
180
+ case 'customAddress':
181
+ case 'customIPAddress':
182
+ if (typeof v !== 'string' || v === '') {
183
+ logIgnore(`${key}.${id}.${k}`)
184
+ } else {
185
+ platform.deviceConf[id][k] = v.replace(WHITESPACE_REGEX, '')
186
+ }
187
+ break
188
+ case 'deviceId':
189
+ break
190
+ case 'diyMode':
191
+ case 'diyModeTwo':
192
+ case 'diyModeThree':
193
+ case 'diyModeFour':
194
+ case 'musicMode':
195
+ case 'musicModeTwo':
196
+ case 'scene':
197
+ case 'sceneTwo':
198
+ case 'sceneThree':
199
+ case 'sceneFour':
200
+ case 'segmented':
201
+ case 'segmentedTwo':
202
+ case 'segmentedThree':
203
+ case 'segmentedFour':
204
+ case 'temperatureSource':
205
+ case 'videoMode':
206
+ case 'videoModeTwo': {
207
+ if (typeof v === 'string') {
208
+ platform.log.warn(`${key}.${id}.${k} incorrectly configured - please use the config screen to reconfigure this item:`)
209
+ platform.log.warn(`${key}.${id}.${k}: ${v}`)
210
+ }
211
+ if (typeof v === 'object') {
212
+ // object - only allowed keys are 'sceneCode', 'bleCode' and 'showAs'
213
+ const subEntries = Object.entries(v)
214
+ if (subEntries.length > 0) {
215
+ platform.deviceConf[id][k] = {}
216
+ subEntries.forEach((subSubEntry) => {
217
+ const [k1, v1] = subSubEntry
218
+ switch (k1) {
219
+ case 'bleCode':
220
+ case 'sceneCode':
221
+ if (typeof v1 !== 'string' || v1 === '') {
222
+ logIgnore(`${key}.${id}.${k}.${k1}`)
223
+ } else {
224
+ platform.deviceConf[id][k][k1] = v1
225
+ }
226
+ break
227
+ case 'showAs': {
228
+ if (typeof v1 !== 'string' || !['default', 'switch'].includes(v1)) {
229
+ logIgnore(`${key}.${id}.${k}.${k1}`)
230
+ } else {
231
+ platform.deviceConf[id][k][k1] = v1
232
+ }
233
+ break
234
+ }
235
+ default:
236
+ logIgnore(`${key}.${id}.${k}.${k1}`)
237
+ break
238
+ }
239
+ })
240
+ } else {
241
+ logIgnore(`${key}.${id}.${k}`)
242
+ }
243
+ } else {
244
+ logIgnore(`${key}.${id}.${k}`)
245
+ }
246
+ break
247
+ }
248
+ case 'ignoreDevice':
249
+ if (typeof v === 'string') {
250
+ logQuotes(`${key}.${id}.${k}`)
251
+ }
252
+ if (!!v && v !== 'false') {
253
+ platform.ignoredDevices.push(id)
254
+ }
255
+ break
256
+ case 'label':
257
+ if (typeof v !== 'string' || v === '') {
258
+ logIgnore(`${key}.${id}.${k}`)
259
+ } else {
260
+ platform.deviceConf[id][k] = v
261
+ }
262
+ break
263
+ default:
264
+ logRemove(`${key}.${id}.${k}`)
265
+ }
266
+ })
267
+ })
268
+ } else {
269
+ logIgnore(key)
270
+ }
271
+ break
272
+ case 'name':
273
+ case 'platform':
274
+ break
275
+ default:
276
+ logRemove(key)
277
+ break
278
+ }
279
+ })
280
+ }