@smarthome.exchange/integrations 2.0.0 → 3.0.0
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.
- package/changelog.md +35 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/core/classes.nativeintegrationowner.d.ts +2 -0
- package/dist_ts/core/classes.nativeintegrationowner.js +5 -1
- package/dist_ts/core/classes.runtimemanager.d.ts +2 -0
- package/dist_ts/core/classes.runtimemanager.js +11 -3
- package/dist_ts/core/types.d.ts +2 -0
- package/dist_ts/integrations/deconz/deconz.classes.client.d.ts +37 -29
- package/dist_ts/integrations/deconz/deconz.classes.client.js +521 -294
- package/dist_ts/integrations/deconz/deconz.classes.configflow.d.ts +6 -1
- package/dist_ts/integrations/deconz/deconz.classes.configflow.js +63 -33
- package/dist_ts/integrations/deconz/deconz.classes.integration.d.ts +4 -3
- package/dist_ts/integrations/deconz/deconz.classes.integration.js +85 -40
- package/dist_ts/integrations/deconz/deconz.mapper.d.ts +1 -0
- package/dist_ts/integrations/deconz/deconz.mapper.js +24 -6
- package/dist_ts/integrations/deconz/deconz.types.d.ts +43 -6
- package/dist_ts/integrations/deconz/deconz.validation.d.ts +7 -1
- package/dist_ts/integrations/deconz/deconz.validation.js +215 -4
- package/dist_ts/integrations/esphome/esphome.classes.client.d.ts +19 -7
- package/dist_ts/integrations/esphome/esphome.classes.client.js +183 -89
- package/dist_ts/integrations/esphome/esphome.classes.configflow.d.ts +5 -2
- package/dist_ts/integrations/esphome/esphome.classes.configflow.js +43 -33
- package/dist_ts/integrations/esphome/esphome.classes.integration.d.ts +8 -6
- package/dist_ts/integrations/esphome/esphome.classes.integration.js +71 -43
- package/dist_ts/integrations/esphome/esphome.discovery.js +4 -12
- package/dist_ts/integrations/esphome/esphome.mapper.d.ts +2 -2
- package/dist_ts/integrations/esphome/esphome.mapper.js +6 -4
- package/dist_ts/integrations/esphome/esphome.types.d.ts +31 -6
- package/dist_ts/integrations/esphome/esphome.types.js +1 -1
- package/dist_ts/integrations/esphome/esphome.validation.d.ts +7 -0
- package/dist_ts/integrations/esphome/esphome.validation.js +66 -0
- package/dist_ts/integrations/homeassistant/homeassistant.classes.client.d.ts +32 -4
- package/dist_ts/integrations/homeassistant/homeassistant.classes.client.js +345 -116
- package/dist_ts/integrations/homeassistant/homeassistant.classes.integration.d.ts +8 -5
- package/dist_ts/integrations/homeassistant/homeassistant.classes.integration.js +34 -8
- package/dist_ts/integrations/homeassistant/homeassistant.classes.runtime.d.ts +7 -4
- package/dist_ts/integrations/homeassistant/homeassistant.classes.runtime.js +44 -56
- package/dist_ts/integrations/homeassistant/homeassistant.types.d.ts +12 -1
- package/dist_ts/integrations/hue/hue.classes.client.d.ts +6 -1
- package/dist_ts/integrations/hue/hue.classes.client.js +120 -45
- package/dist_ts/integrations/hue/hue.classes.configflow.js +16 -7
- package/dist_ts/integrations/hue/hue.classes.integration.d.ts +2 -0
- package/dist_ts/integrations/hue/hue.classes.integration.js +103 -30
- package/dist_ts/integrations/hue/hue.validation.d.ts +1 -0
- package/dist_ts/integrations/hue/hue.validation.js +14 -4
- package/dist_ts/integrations/mqtt/mqtt.classes.client.d.ts +15 -2
- package/dist_ts/integrations/mqtt/mqtt.classes.client.js +173 -23
- package/dist_ts/integrations/mqtt/mqtt.classes.configflow.d.ts +5 -1
- package/dist_ts/integrations/mqtt/mqtt.classes.configflow.js +53 -33
- package/dist_ts/integrations/mqtt/mqtt.classes.integration.d.ts +4 -1
- package/dist_ts/integrations/mqtt/mqtt.classes.integration.js +127 -56
- package/dist_ts/integrations/mqtt/mqtt.classes.session.d.ts +13 -0
- package/dist_ts/integrations/mqtt/mqtt.classes.session.js +130 -0
- package/dist_ts/integrations/mqtt/mqtt.mapper.d.ts +3 -0
- package/dist_ts/integrations/mqtt/mqtt.mapper.js +44 -1
- package/dist_ts/integrations/mqtt/mqtt.types.d.ts +46 -0
- package/dist_ts/integrations/mqtt/mqtt.validation.d.ts +7 -0
- package/dist_ts/integrations/mqtt/mqtt.validation.js +180 -0
- package/dist_ts/integrations/samsungtv/samsungtv.classes.client.d.ts +4 -2
- package/dist_ts/integrations/samsungtv/samsungtv.classes.client.js +41 -16
- package/dist_ts/integrations/samsungtv/samsungtv.classes.configflow.d.ts +5 -0
- package/dist_ts/integrations/samsungtv/samsungtv.classes.configflow.js +20 -7
- package/dist_ts/integrations/samsungtv/samsungtv.classes.integration.d.ts +1 -0
- package/dist_ts/integrations/samsungtv/samsungtv.classes.integration.js +53 -50
- package/dist_ts/integrations/samsungtv/samsungtv.types.d.ts +4 -1
- package/dist_ts/integrations/shelly/shelly.classes.client.d.ts +26 -30
- package/dist_ts/integrations/shelly/shelly.classes.client.js +504 -276
- package/dist_ts/integrations/shelly/shelly.classes.configflow.d.ts +8 -3
- package/dist_ts/integrations/shelly/shelly.classes.configflow.js +43 -38
- package/dist_ts/integrations/shelly/shelly.classes.integration.d.ts +6 -6
- package/dist_ts/integrations/shelly/shelly.classes.integration.js +103 -101
- package/dist_ts/integrations/shelly/shelly.classes.subscription.d.ts +4 -0
- package/dist_ts/integrations/shelly/shelly.classes.subscription.js +24 -2
- package/dist_ts/integrations/shelly/shelly.mapper.d.ts +1 -2
- package/dist_ts/integrations/shelly/shelly.mapper.js +21 -20
- package/dist_ts/integrations/shelly/shelly.rpc.js +4 -1
- package/dist_ts/integrations/shelly/shelly.types.d.ts +42 -2
- package/dist_ts/integrations/shelly/shelly.types.js +11 -2
- package/dist_ts/integrations/shelly/shelly.validation.d.ts +7 -0
- package/dist_ts/integrations/shelly/shelly.validation.js +79 -0
- package/dist_ts/integrations/sonos/sonos.classes.client.d.ts +79 -22
- package/dist_ts/integrations/sonos/sonos.classes.client.js +427 -139
- package/dist_ts/integrations/sonos/sonos.classes.configflow.d.ts +9 -2
- package/dist_ts/integrations/sonos/sonos.classes.configflow.js +26 -28
- package/dist_ts/integrations/sonos/sonos.classes.integration.d.ts +7 -6
- package/dist_ts/integrations/sonos/sonos.classes.integration.js +289 -224
- package/dist_ts/integrations/sonos/sonos.config.d.ts +6 -1
- package/dist_ts/integrations/sonos/sonos.config.js +23 -2
- package/dist_ts/integrations/sonos/sonos.mapper.d.ts +1 -1
- package/dist_ts/integrations/sonos/sonos.mapper.js +14 -9
- package/dist_ts/integrations/sonos/sonos.matchers.js +3 -3
- package/dist_ts/integrations/sonos/sonos.types.d.ts +26 -1
- package/dist_ts/integrations/tasmota/tasmota.classes.integration.d.ts +5 -0
- package/dist_ts/integrations/tasmota/tasmota.classes.integration.js +23 -7
- package/dist_ts/integrations/tasmota/tasmota.classes.runtime.d.ts +19 -5
- package/dist_ts/integrations/tasmota/tasmota.classes.runtime.js +115 -41
- package/dist_ts/integrations/tasmota/tasmota.config.js +23 -1
- package/dist_ts/integrations/tasmota/tasmota.credentials.d.ts +11 -0
- package/dist_ts/integrations/tasmota/tasmota.credentials.js +97 -0
- package/dist_ts/integrations/tasmota/tasmota.discovery.d.ts +7 -1
- package/dist_ts/integrations/tasmota/tasmota.discovery.js +108 -30
- package/dist_ts/integrations/tasmota/tasmota.mapper.d.ts +2 -2
- package/dist_ts/integrations/tasmota/tasmota.mapper.js +7 -7
- package/dist_ts/integrations/tasmota/tasmota.types.d.ts +41 -2
- package/dist_ts/integrations/tuya/tuya.classes.integration.d.ts +4 -1
- package/dist_ts/integrations/tuya/tuya.classes.integration.js +79 -110
- package/dist_ts/integrations/tuya/tuya.classes.runtime.d.ts +5 -2
- package/dist_ts/integrations/tuya/tuya.classes.runtime.js +73 -26
- package/dist_ts/integrations/tuya/tuya.mapper.js +2 -2
- package/dist_ts/integrations/tuya/tuya.types.d.ts +3 -0
- package/dist_ts/integrations/tuya/tuya.validation.d.ts +2 -2
- package/dist_ts/integrations/tuya/tuya.validation.js +6 -11
- package/dist_ts/integrations/webostv/webostv.classes.client.d.ts +6 -5
- package/dist_ts/integrations/webostv/webostv.classes.client.js +147 -76
- package/dist_ts/integrations/webostv/webostv.classes.configflow.d.ts +7 -2
- package/dist_ts/integrations/webostv/webostv.classes.configflow.js +36 -20
- package/dist_ts/integrations/webostv/webostv.classes.integration.d.ts +4 -4
- package/dist_ts/integrations/webostv/webostv.classes.integration.js +46 -54
- package/dist_ts/integrations/webostv/webostv.mapper.js +5 -5
- package/dist_ts/integrations/webostv/webostv.types.d.ts +16 -3
- package/dist_ts/integrations/webostv/webostv.types.js +1 -1
- package/dist_ts/integrations/webostv/webostv.validation.d.ts +3 -1
- package/dist_ts/integrations/webostv/webostv.validation.js +23 -1
- package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.configflow.d.ts +6 -1
- package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.configflow.js +18 -8
- package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.integration.d.ts +4 -1
- package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.integration.js +54 -6
- package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.runtime.d.ts +12 -6
- package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.runtime.js +73 -59
- package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.mapper.d.ts +3 -2
- package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.mapper.js +17 -12
- package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.types.d.ts +31 -2
- package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.validation.d.ts +9 -1
- package/dist_ts/integrations/zigbee2mqtt/zigbee2mqtt.validation.js +57 -7
- package/dist_ts/integrations/zwave_js/zwave_js.classes.client.d.ts +10 -4
- package/dist_ts/integrations/zwave_js/zwave_js.classes.client.js +167 -31
- package/dist_ts/integrations/zwave_js/zwave_js.classes.configflow.d.ts +6 -1
- package/dist_ts/integrations/zwave_js/zwave_js.classes.configflow.js +57 -35
- package/dist_ts/integrations/zwave_js/zwave_js.classes.integration.d.ts +3 -1
- package/dist_ts/integrations/zwave_js/zwave_js.classes.integration.js +59 -37
- package/dist_ts/integrations/zwave_js/zwave_js.discovery.js +3 -3
- package/dist_ts/integrations/zwave_js/zwave_js.mapper.js +41 -24
- package/dist_ts/integrations/zwave_js/zwave_js.types.d.ts +25 -3
- package/dist_ts/integrations/zwave_js/zwave_js.validation.d.ts +6 -2
- package/dist_ts/integrations/zwave_js/zwave_js.validation.js +52 -17
- package/dist_ts/plugins.d.ts +2 -1
- package/dist_ts/plugins.js +3 -2
- package/package.json +8 -8
- package/readme.md +25 -6
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/core/classes.nativeintegrationowner.ts +5 -0
- package/ts/core/classes.runtimemanager.ts +11 -2
- package/ts/core/types.ts +2 -0
- package/ts/integrations/deconz/deconz.classes.client.ts +348 -252
- package/ts/integrations/deconz/deconz.classes.configflow.ts +57 -29
- package/ts/integrations/deconz/deconz.classes.integration.ts +69 -37
- package/ts/integrations/deconz/deconz.mapper.ts +22 -5
- package/ts/integrations/deconz/deconz.types.ts +49 -6
- package/ts/integrations/deconz/deconz.validation.ts +177 -4
- package/ts/integrations/deconz/readme.md +15 -1
- package/ts/integrations/esphome/esphome.classes.client.ts +179 -94
- package/ts/integrations/esphome/esphome.classes.configflow.ts +40 -33
- package/ts/integrations/esphome/esphome.classes.integration.ts +63 -36
- package/ts/integrations/esphome/esphome.discovery.ts +3 -12
- package/ts/integrations/esphome/esphome.mapper.ts +9 -7
- package/ts/integrations/esphome/esphome.types.ts +35 -6
- package/ts/integrations/esphome/esphome.validation.ts +66 -0
- package/ts/integrations/esphome/readme.md +20 -6
- package/ts/integrations/homeassistant/homeassistant.classes.client.ts +353 -125
- package/ts/integrations/homeassistant/homeassistant.classes.integration.ts +35 -18
- package/ts/integrations/homeassistant/homeassistant.classes.runtime.ts +50 -63
- package/ts/integrations/homeassistant/homeassistant.types.ts +17 -1
- package/ts/integrations/homeassistant/readme.md +22 -6
- package/ts/integrations/hue/hue.classes.client.ts +110 -41
- package/ts/integrations/hue/hue.classes.configflow.ts +12 -6
- package/ts/integrations/hue/hue.classes.integration.ts +92 -26
- package/ts/integrations/hue/hue.validation.ts +13 -3
- package/ts/integrations/hue/readme.md +3 -3
- package/ts/integrations/mqtt/mqtt.classes.client.ts +136 -19
- package/ts/integrations/mqtt/mqtt.classes.configflow.ts +45 -32
- package/ts/integrations/mqtt/mqtt.classes.integration.ts +99 -38
- package/ts/integrations/mqtt/mqtt.classes.session.ts +118 -0
- package/ts/integrations/mqtt/mqtt.mapper.ts +43 -1
- package/ts/integrations/mqtt/mqtt.types.ts +52 -0
- package/ts/integrations/mqtt/mqtt.validation.ts +157 -0
- package/ts/integrations/mqtt/readme.md +12 -3
- package/ts/integrations/samsungtv/readme.md +11 -5
- package/ts/integrations/samsungtv/samsungtv.classes.client.ts +40 -15
- package/ts/integrations/samsungtv/samsungtv.classes.configflow.ts +21 -6
- package/ts/integrations/samsungtv/samsungtv.classes.integration.ts +53 -38
- package/ts/integrations/samsungtv/samsungtv.types.ts +4 -1
- package/ts/integrations/shelly/readme.md +22 -7
- package/ts/integrations/shelly/shelly.classes.client.ts +386 -269
- package/ts/integrations/shelly/shelly.classes.configflow.ts +43 -38
- package/ts/integrations/shelly/shelly.classes.integration.ts +92 -111
- package/ts/integrations/shelly/shelly.classes.subscription.ts +24 -1
- package/ts/integrations/shelly/shelly.mapper.ts +19 -20
- package/ts/integrations/shelly/shelly.rpc.ts +2 -0
- package/ts/integrations/shelly/shelly.types.ts +50 -2
- package/ts/integrations/shelly/shelly.validation.ts +76 -0
- package/ts/integrations/sonos/readme.md +117 -38
- package/ts/integrations/sonos/sonos.classes.client.ts +392 -123
- package/ts/integrations/sonos/sonos.classes.configflow.ts +25 -28
- package/ts/integrations/sonos/sonos.classes.integration.ts +211 -186
- package/ts/integrations/sonos/sonos.config.ts +24 -2
- package/ts/integrations/sonos/sonos.mapper.ts +13 -8
- package/ts/integrations/sonos/sonos.matchers.ts +2 -2
- package/ts/integrations/sonos/sonos.types.ts +30 -1
- package/ts/integrations/tasmota/readme.md +16 -5
- package/ts/integrations/tasmota/tasmota.classes.integration.ts +24 -6
- package/ts/integrations/tasmota/tasmota.classes.runtime.ts +112 -43
- package/ts/integrations/tasmota/tasmota.config.ts +17 -0
- package/ts/integrations/tasmota/tasmota.credentials.ts +92 -0
- package/ts/integrations/tasmota/tasmota.discovery.ts +87 -24
- package/ts/integrations/tasmota/tasmota.mapper.ts +6 -6
- package/ts/integrations/tasmota/tasmota.types.ts +43 -2
- package/ts/integrations/tuya/readme.md +52 -10
- package/ts/integrations/tuya/tuya.classes.integration.ts +69 -98
- package/ts/integrations/tuya/tuya.classes.runtime.ts +70 -24
- package/ts/integrations/tuya/tuya.mapper.ts +1 -1
- package/ts/integrations/tuya/tuya.types.ts +4 -0
- package/ts/integrations/tuya/tuya.validation.ts +5 -8
- package/ts/integrations/webostv/readme.md +115 -22
- package/ts/integrations/webostv/webostv.classes.client.ts +123 -50
- package/ts/integrations/webostv/webostv.classes.configflow.ts +35 -15
- package/ts/integrations/webostv/webostv.classes.integration.ts +44 -37
- package/ts/integrations/webostv/webostv.mapper.ts +4 -4
- package/ts/integrations/webostv/webostv.types.ts +18 -3
- package/ts/integrations/webostv/webostv.validation.ts +22 -1
- package/ts/integrations/zigbee2mqtt/readme.md +22 -27
- package/ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.configflow.ts +16 -7
- package/ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.integration.ts +53 -6
- package/ts/integrations/zigbee2mqtt/zigbee2mqtt.classes.runtime.ts +62 -49
- package/ts/integrations/zigbee2mqtt/zigbee2mqtt.mapper.ts +17 -11
- package/ts/integrations/zigbee2mqtt/zigbee2mqtt.types.ts +35 -2
- package/ts/integrations/zigbee2mqtt/zigbee2mqtt.validation.ts +53 -7
- package/ts/integrations/zwave_js/readme.md +42 -9
- package/ts/integrations/zwave_js/zwave_js.classes.client.ts +155 -31
- package/ts/integrations/zwave_js/zwave_js.classes.configflow.ts +55 -31
- package/ts/integrations/zwave_js/zwave_js.classes.integration.ts +57 -35
- package/ts/integrations/zwave_js/zwave_js.discovery.ts +2 -2
- package/ts/integrations/zwave_js/zwave_js.mapper.ts +37 -23
- package/ts/integrations/zwave_js/zwave_js.types.ts +26 -3
- package/ts/integrations/zwave_js/zwave_js.validation.ts +52 -18
- package/ts/plugins.ts +2 -1
|
@@ -2,6 +2,7 @@ import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
|
|
|
2
2
|
import type { IDiscoveryCandidate, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
|
|
3
3
|
import type { IEsphomeManualEntry, IEsphomeMdnsRecord } from './esphome.types.js';
|
|
4
4
|
import { esphomeDefaultNativeApiPort } from './esphome.types.js';
|
|
5
|
+
import { esphomeMac } from './esphome.validation.js';
|
|
5
6
|
|
|
6
7
|
const esphomeMdnsTypes = new Set(['_esphomelib._tcp.local', '_esphome._tcp.local']);
|
|
7
8
|
|
|
@@ -37,7 +38,6 @@ export class EsphomeMdnsMatcher implements IDiscoveryMatcher<IEsphomeMdnsRecord>
|
|
|
37
38
|
metadata: {
|
|
38
39
|
mdnsName: recordArg.name,
|
|
39
40
|
mdnsType: recordArg.type,
|
|
40
|
-
txt,
|
|
41
41
|
deviceName,
|
|
42
42
|
encryptionRequired: Boolean(this.txt(txt, 'api_encryption')),
|
|
43
43
|
},
|
|
@@ -63,13 +63,8 @@ export class EsphomeMdnsMatcher implements IDiscoveryMatcher<IEsphomeMdnsRecord>
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
private normalizeMac(valueArg?: string): string | undefined {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
const compact = valueArg.replace(/[^a-fA-F0-9]/g, '').toLowerCase();
|
|
70
|
-
if (compact.length !== 12) {
|
|
71
|
-
return valueArg.toLowerCase();
|
|
72
|
-
}
|
|
66
|
+
const compact = esphomeMac(valueArg);
|
|
67
|
+
if (!compact) return undefined;
|
|
73
68
|
return compact.match(/.{1,2}/g)?.join(':');
|
|
74
69
|
}
|
|
75
70
|
}
|
|
@@ -101,11 +96,7 @@ export class EsphomeManualMatcher implements IDiscoveryMatcher<IEsphomeManualEnt
|
|
|
101
96
|
manufacturer: inputArg.manufacturer || 'ESPHome',
|
|
102
97
|
model: inputArg.model || 'Native API device',
|
|
103
98
|
metadata: {
|
|
104
|
-
...inputArg.metadata,
|
|
105
99
|
deviceName: inputArg.deviceName,
|
|
106
|
-
password: inputArg.password,
|
|
107
|
-
encryptionKey: inputArg.encryptionKey,
|
|
108
|
-
noisePsk: inputArg.noisePsk,
|
|
109
100
|
},
|
|
110
101
|
},
|
|
111
102
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as plugins from '../../plugins.js';
|
|
2
2
|
import type { IIntegrationEntity, IServiceCallRequest, TEntityPlatform } from '../../core/types.js';
|
|
3
|
-
import type { IEsphomeAreaInfo, IEsphomeClientCommand,
|
|
3
|
+
import type { IEsphomeAreaInfo, IEsphomeClientCommand, IEsphomeMappingConfig, IEsphomeDeviceInfo, IEsphomeEntityDescriptor, IEsphomeEntityState, IEsphomeManualEntry, IEsphomeSnapshot } from './esphome.types.js';
|
|
4
4
|
import { esphomeDefaultNativeApiPort } from './esphome.types.js';
|
|
5
5
|
|
|
6
6
|
const serviceDataFields = new Map<string, readonly string[]>([
|
|
@@ -18,11 +18,12 @@ const serviceDataFields = new Map<string, readonly string[]>([
|
|
|
18
18
|
]);
|
|
19
19
|
|
|
20
20
|
export class EsphomeMapper {
|
|
21
|
-
public static toSnapshot(configArg:
|
|
21
|
+
public static toSnapshot(configArg: IEsphomeMappingConfig, connectedArg?: boolean, eventsArg: IEsphomeSnapshot['events'] = []): IEsphomeSnapshot {
|
|
22
22
|
const source = configArg.snapshot;
|
|
23
23
|
const manualEntry = configArg.manualEntries?.[0];
|
|
24
24
|
const deviceInfo = this.deviceInfoFromConfig(configArg, source?.deviceInfo, manualEntry);
|
|
25
25
|
return {
|
|
26
|
+
homeId: configArg.homeId ?? source?.homeId,
|
|
26
27
|
host: configArg.host || source?.host || manualEntry?.host || deviceInfo.host,
|
|
27
28
|
port: configArg.port || source?.port || manualEntry?.port || deviceInfo.port || esphomeDefaultNativeApiPort,
|
|
28
29
|
connected: connectedArg ?? source?.connected ?? false,
|
|
@@ -216,7 +217,7 @@ export class EsphomeMapper {
|
|
|
216
217
|
return `esphome_${this.slug(`${deviceIdentifier}_${entityArg.platform}_${objectId}_${entityArg.key}_${subDevice ?? 0}`)}`;
|
|
217
218
|
}
|
|
218
219
|
|
|
219
|
-
private static deviceInfoFromConfig(configArg:
|
|
220
|
+
private static deviceInfoFromConfig(configArg: IEsphomeMappingConfig, sourceArg?: IEsphomeDeviceInfo, manualEntryArg?: IEsphomeManualEntry): IEsphomeDeviceInfo {
|
|
220
221
|
return {
|
|
221
222
|
...sourceArg,
|
|
222
223
|
...configArg.deviceInfo,
|
|
@@ -226,12 +227,12 @@ export class EsphomeMapper {
|
|
|
226
227
|
model: configArg.deviceInfo?.model || sourceArg?.model || manualEntryArg?.model,
|
|
227
228
|
host: configArg.host || sourceArg?.host || manualEntryArg?.host || configArg.deviceInfo?.host,
|
|
228
229
|
port: configArg.port || sourceArg?.port || manualEntryArg?.port || configArg.deviceInfo?.port || esphomeDefaultNativeApiPort,
|
|
229
|
-
usesPassword: configArg.deviceInfo?.usesPassword ?? sourceArg?.usesPassword
|
|
230
|
-
apiEncryptionSupported: configArg.deviceInfo?.apiEncryptionSupported ?? sourceArg?.apiEncryptionSupported
|
|
230
|
+
usesPassword: configArg.deviceInfo?.usesPassword ?? sourceArg?.usesPassword,
|
|
231
|
+
apiEncryptionSupported: configArg.deviceInfo?.apiEncryptionSupported ?? sourceArg?.apiEncryptionSupported,
|
|
231
232
|
};
|
|
232
233
|
}
|
|
233
234
|
|
|
234
|
-
private static normalizeStates(statesArg:
|
|
235
|
+
private static normalizeStates(statesArg: IEsphomeMappingConfig['states'] | IEsphomeSnapshot['states'] | undefined): IEsphomeEntityState[] {
|
|
235
236
|
if (!statesArg) {
|
|
236
237
|
return [];
|
|
237
238
|
}
|
|
@@ -450,7 +451,8 @@ export class EsphomeMapper {
|
|
|
450
451
|
}
|
|
451
452
|
|
|
452
453
|
private static mainDeviceId(snapshotArg: IEsphomeSnapshot): string {
|
|
453
|
-
|
|
454
|
+
const scope = snapshotArg.homeId ? `home.${encodeURIComponent(snapshotArg.homeId)}.` : '';
|
|
455
|
+
return `${scope}esphome.device.${this.slug(this.compactDeviceIdentifier(snapshotArg.deviceInfo) || snapshotArg.deviceInfo.name || snapshotArg.host || 'configured')}`;
|
|
454
456
|
}
|
|
455
457
|
|
|
456
458
|
private static subDeviceId(snapshotArg: IEsphomeSnapshot, subDeviceIdArg: string | number): string {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TEntityPlatform } from '../../core/types.js';
|
|
2
|
+
import type * as plugins from '../../plugins.js';
|
|
2
3
|
|
|
3
4
|
export const esphomeDefaultNativeApiPort = 6053;
|
|
4
5
|
|
|
@@ -16,13 +17,43 @@ export type TEsphomeEntityPlatform =
|
|
|
16
17
|
| 'water_heater';
|
|
17
18
|
|
|
18
19
|
export interface IEsphomeConfig {
|
|
20
|
+
readonly homeId: string;
|
|
21
|
+
readonly host: string;
|
|
22
|
+
readonly port?: number;
|
|
23
|
+
readonly deviceName: string;
|
|
24
|
+
readonly expectedMacAddress: string;
|
|
25
|
+
readonly expectedModel?: string;
|
|
26
|
+
readonly name?: string;
|
|
27
|
+
readonly credentialKey?: string;
|
|
28
|
+
readonly credentialSource?: 'provider' | 'store';
|
|
29
|
+
readonly initialSnapshotTimeoutMs?: number;
|
|
30
|
+
readonly commandTimeoutMs?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Only the published native client's Noise option is supported; legacy passwords are not. */
|
|
34
|
+
export type TEsphomeCredentials = Readonly<Pick<plugins.esphomeNativeApi.EspHomeClientOptions, 'psk'>>;
|
|
35
|
+
|
|
36
|
+
export interface IEsphomeCredentialRequest {
|
|
37
|
+
readonly capturedHomeId: string;
|
|
38
|
+
readonly credentialKey: string;
|
|
39
|
+
readonly signal: AbortSignal;
|
|
40
|
+
/** Absolute milliseconds on performance.now(), including provisioning and native setup. */
|
|
41
|
+
readonly monotonicDeadline: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface IEsphomeIntegrationOptions {
|
|
45
|
+
readonly homeId?: string;
|
|
46
|
+
readonly capacity?: number;
|
|
47
|
+
readonly resolveCredentials?: (requestArg: IEsphomeCredentialRequest) => Promise<TEsphomeCredentials>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Pure mapper input. This cannot be used for live setup. */
|
|
51
|
+
export interface IEsphomeMappingConfig {
|
|
52
|
+
homeId?: string;
|
|
19
53
|
host?: string;
|
|
20
54
|
port?: number;
|
|
21
55
|
name?: string;
|
|
22
56
|
deviceName?: string;
|
|
23
|
-
password?: string;
|
|
24
|
-
encryptionKey?: string;
|
|
25
|
-
noisePsk?: string;
|
|
26
57
|
initialSnapshotTimeoutMs?: number;
|
|
27
58
|
commandTimeoutMs?: number;
|
|
28
59
|
snapshot?: IEsphomeSnapshot;
|
|
@@ -151,6 +182,7 @@ export interface IEsphomeEntityState {
|
|
|
151
182
|
}
|
|
152
183
|
|
|
153
184
|
export interface IEsphomeSnapshot {
|
|
185
|
+
homeId?: string;
|
|
154
186
|
host?: string;
|
|
155
187
|
port: number;
|
|
156
188
|
connected: boolean;
|
|
@@ -236,9 +268,6 @@ export interface IEsphomeManualEntry {
|
|
|
236
268
|
deviceName?: string;
|
|
237
269
|
model?: string;
|
|
238
270
|
manufacturer?: string;
|
|
239
|
-
password?: string;
|
|
240
|
-
encryptionKey?: string;
|
|
241
|
-
noisePsk?: string;
|
|
242
271
|
metadata?: Record<string, unknown>;
|
|
243
272
|
}
|
|
244
273
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { IEsphomeConfig, TEsphomeCredentials } from './esphome.types.js';
|
|
2
|
+
|
|
3
|
+
export const esphomeData = (valueArg: unknown, fieldsArg: readonly string[], labelArg: string): Record<string, unknown> => {
|
|
4
|
+
if (!valueArg || typeof valueArg !== 'object' || (Object.getPrototypeOf(valueArg) !== Object.prototype && Object.getPrototypeOf(valueArg) !== null)) {
|
|
5
|
+
throw new TypeError(`${labelArg} must be a plain data object.`);
|
|
6
|
+
}
|
|
7
|
+
const captured: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
|
|
8
|
+
for (const key of Reflect.ownKeys(valueArg)) {
|
|
9
|
+
const field = Object.getOwnPropertyDescriptor(valueArg, key);
|
|
10
|
+
if (typeof key !== 'string' || !fieldsArg.includes(key) || !field || !field.enumerable || !('value' in field)) {
|
|
11
|
+
throw new TypeError(`${labelArg} contains an unsupported field or accessor.`);
|
|
12
|
+
}
|
|
13
|
+
captured[key] = field.value as unknown;
|
|
14
|
+
}
|
|
15
|
+
return captured;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const esphomeIdentifier = (valueArg: unknown, labelArg: string): string => {
|
|
19
|
+
if (typeof valueArg !== 'string' || !valueArg || valueArg.trim() !== valueArg || valueArg.length > 256 || /[\p{Cc}\p{Cs}]/u.test(valueArg)) {
|
|
20
|
+
throw new TypeError(`${labelArg} must be a non-empty identifier without surrounding whitespace or control characters.`);
|
|
21
|
+
}
|
|
22
|
+
return valueArg;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const esphomeTimeout = (valueArg: unknown, defaultArg: number): number => {
|
|
26
|
+
const value = valueArg ?? defaultArg;
|
|
27
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 1 || value > 2147483647) {
|
|
28
|
+
throw new TypeError('ESPHome timeouts must be integers from 1 through 2147483647.');
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const esphomeMac = (valueArg: unknown): string | undefined => {
|
|
34
|
+
if (typeof valueArg !== 'string' || !/^(?:[a-f\d]{12}|(?:[a-f\d]{2}:){5}[a-f\d]{2})$/i.test(valueArg)) return undefined;
|
|
35
|
+
return valueArg.replace(/:/g, '').toLowerCase();
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const captureEsphomeConfig = (valueArg: IEsphomeConfig): IEsphomeConfig => {
|
|
39
|
+
const value = esphomeData(valueArg, ['homeId', 'host', 'port', 'name', 'deviceName', 'expectedMacAddress', 'expectedModel', 'credentialKey', 'credentialSource', 'initialSnapshotTimeoutMs', 'commandTimeoutMs'], 'ESPHome configuration');
|
|
40
|
+
const host = esphomeIdentifier(value.host, 'ESPHome host');
|
|
41
|
+
if (/\s/.test(host)) throw new TypeError('ESPHome host must not contain whitespace.');
|
|
42
|
+
const port = value.port ?? 6053;
|
|
43
|
+
if (typeof port !== 'number' || !Number.isInteger(port) || port < 1 || port > 65535) throw new TypeError('ESPHome port must be an integer from 1 through 65535.');
|
|
44
|
+
const mac = esphomeMac(value.expectedMacAddress);
|
|
45
|
+
if (!mac) throw new TypeError('ESPHome expectedMacAddress must be a complete MAC address.');
|
|
46
|
+
if (value.credentialSource !== undefined && value.credentialSource !== 'provider' && value.credentialSource !== 'store') throw new TypeError('ESPHome credential source must be provider or store.');
|
|
47
|
+
if (value.credentialSource !== undefined && value.credentialKey === undefined) throw new TypeError('ESPHome credential source requires a credential reference.');
|
|
48
|
+
return Object.freeze({
|
|
49
|
+
homeId: esphomeIdentifier(value.homeId, 'ESPHome configuration homeId'), host, port,
|
|
50
|
+
deviceName: esphomeIdentifier(value.deviceName, 'ESPHome selected device name'), expectedMacAddress: mac,
|
|
51
|
+
expectedModel: value.expectedModel === undefined ? undefined : esphomeIdentifier(value.expectedModel, 'ESPHome selected model'),
|
|
52
|
+
name: value.name === undefined ? undefined : esphomeIdentifier(value.name, 'ESPHome display name'),
|
|
53
|
+
credentialKey: value.credentialKey === undefined ? undefined : esphomeIdentifier(value.credentialKey, 'ESPHome credential reference'),
|
|
54
|
+
credentialSource: value.credentialSource as 'provider' | 'store' | undefined,
|
|
55
|
+
initialSnapshotTimeoutMs: esphomeTimeout(value.initialSnapshotTimeoutMs, 15000), commandTimeoutMs: esphomeTimeout(value.commandTimeoutMs, 5000),
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const captureEsphomeCredentials = (valueArg: unknown): TEsphomeCredentials => {
|
|
60
|
+
const value = esphomeData(valueArg, ['psk'], 'ESPHome credential provider result');
|
|
61
|
+
const psk = value.psk;
|
|
62
|
+
if (psk !== undefined && psk !== null && (typeof psk !== 'string' || !/^[A-Za-z0-9+/]{43}=$/.test(psk) || Buffer.from(psk, 'base64').length !== 32 || Buffer.from(psk, 'base64').toString('base64') !== psk)) {
|
|
63
|
+
throw new TypeError('ESPHome Noise credentials require a canonical base64-encoded 32-byte key.');
|
|
64
|
+
}
|
|
65
|
+
return Object.freeze({ psk: psk as string | null | undefined });
|
|
66
|
+
};
|
|
@@ -10,15 +10,25 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|
|
10
10
|
|
|
11
11
|
Install the parent package with `pnpm add @smarthome.exchange/integrations`. The native client requires Node.js 22.20 or newer and ESPHome 2025.10 or newer. Configure one native API node per integration instance; its advertised logical sub-devices share that connection.
|
|
12
12
|
|
|
13
|
-
Provide a host and
|
|
13
|
+
Provide `homeId`, a host, the selected native `deviceName`, and `expectedMacAddress`; the optional port defaults to `6053`. An optional `expectedModel` must match the model in the actual native device-info response. Setup checks these observed identity fields before publishing readiness, including after reconnect. Discovery supplies endpoint hints and does not prove identity or select the owning home. Home and device/entity routing identifiers include the independently admitted home. MAC/name checks are selection constraints, not authentication; use Noise encryption for authenticated transport.
|
|
14
|
+
|
|
15
|
+
The embedding host must independently supply the same `homeId` in setup and config-flow contexts. An optional constructor home constrains this choice and never substitutes for context. The first valid admission binds an otherwise unbound integration, even when setup fails. Construction performs no network or persistence work. Shared ownership defaults to sixteen pending operations, live runtimes and retained cleanup resources.
|
|
16
|
+
|
|
17
|
+
Configuration contains only an optional `credentialKey` reference. Resolve it through the constructor's borrowed asynchronous `resolveCredentials` callback, which receives the captured home, reference, cancellation signal and an absolute `performance.now()` deadline. Its result supports the published client's `{ psk }` option only: a canonical base64-encoded 32-byte Noise key, or no key for a plaintext endpoint. The entire setup deadline includes credential provisioning. Cancellation joins the actual provider promise and rejects late results before transport construction; an uncooperative provider can therefore delay shutdown. Provider errors are replaced with fixed diagnostics to avoid exposing secrets.
|
|
14
18
|
|
|
15
19
|
```typescript
|
|
16
20
|
import { EsphomeIntegration } from '@smarthome.exchange/integrations';
|
|
17
21
|
|
|
18
|
-
async function inspectNode(host: string,
|
|
19
|
-
const integration = new EsphomeIntegration(
|
|
22
|
+
async function inspectNode(homeId: string, host: string, macAddress: string, resolveNoiseKey: (reference: string, signal: AbortSignal) => Promise<string>) {
|
|
23
|
+
const integration = new EsphomeIntegration({
|
|
24
|
+
resolveCredentials: async ({ credentialKey, signal }) => ({ psk: await resolveNoiseKey(credentialKey, signal) }),
|
|
25
|
+
});
|
|
20
26
|
try {
|
|
21
|
-
const runtime = await integration.setup({
|
|
27
|
+
const runtime = await integration.setup({
|
|
28
|
+
homeId, host, deviceName: 'selected_node', expectedMacAddress: macAddress,
|
|
29
|
+
credentialKey: 'selected-node-noise',
|
|
30
|
+
}, {
|
|
31
|
+
homeId,
|
|
22
32
|
abortSignal: AbortSignal.timeout(15_000),
|
|
23
33
|
});
|
|
24
34
|
return await runtime.devices();
|
|
@@ -28,7 +38,9 @@ async function inspectNode(host: string, encryptionKey: string) {
|
|
|
28
38
|
}
|
|
29
39
|
```
|
|
30
40
|
|
|
31
|
-
Obtain encryption keys through the application's secret provisioning flow.
|
|
41
|
+
Obtain encryption keys through the application's secret provisioning flow. Forms accept references, and discovery never copies passwords, keys or arbitrary TXT/manual metadata. Raw `encryptionKey`, `noisePsk`, legacy passwords, injected snapshots and command executors are rejected in live configuration. A provisioned PSK session that negotiates plaintext is rejected before controls become available. The caller remains responsible for authentication, home access and tool authorization.
|
|
42
|
+
|
|
43
|
+
For an explicitly selected encrypted store reference, set `credentialSource: 'store'` and supply an initialized actual `NoSqlConfigStore` scoped to this home and the `esphome` domain. The caller provisions `{ psk }` under `credentialKey` through the store's public API. The default source is the external provider; neither source falls through to the other on failure. Store reads use the store's configured operation timeout and expose no per-call cancellation option; shutdown joins their actual completion and fences late results. The store, database and encryption-key provider remain caller-owned. No store is needed for an external provider or plaintext setup.
|
|
32
44
|
|
|
33
45
|
## State and controls
|
|
34
46
|
|
|
@@ -38,7 +50,9 @@ Readable platforms are lights, switches, fans, covers, climate entities, numbers
|
|
|
38
50
|
|
|
39
51
|
Use `runtime.writeFeature` with a device and feature returned by that runtime, or `runtime.callService` with an explicit entity or device target. A device target must select exactly one supported entity. Supplying both targets requires them to name the same device. Foreign integration devices, unrelated service domains, out-of-range cover positions, and unknown service settings are rejected. Service data must contain ordinary values without accessors or symbol fields. The `esphome` service domain can select a supported native entity; platform domains must match the selected entity.
|
|
40
52
|
|
|
41
|
-
Each entity admits one pending command. Success requires a subsequent matching state report within `commandTimeoutMs`, which defaults to 5 seconds. A matching report confirms the observed state; the protocol does not supply a unique action receipt or exactly-once physical execution guarantee. Timeout or cancellation can follow a physical effect, so failed or interrupted commands must not be retried automatically. Destruction disconnects the node,
|
|
53
|
+
Each entity admits one pending command. Success requires a subsequent matching state report within `commandTimeoutMs`, which defaults to 5 seconds. A matching report confirms the observed state; the protocol does not supply a unique action receipt or exactly-once physical execution guarantee. Timeout or cancellation can follow a physical effect, so failed or interrupted commands must not be retried automatically. Destruction publishes one stable terminal promise, aborts owned work, disconnects the node, drains pending confirmations and all subscriptions, and retains cleanup failures. `integration.retryCleanup()` retries failed cleanup explicitly without reconnecting or replaying commands. Destruction never closes borrowed credential or persistence resources.
|
|
54
|
+
|
|
55
|
+
The published SDK's synchronous `disconnect()` cancels reconnection, disposes its owned transport and reports disconnect/health transitions. Fixture peers observe the resulting TCP close for plaintext and Noise sessions. The SDK does not expose an OS socket-close completion promise, and this adapter makes no stronger OS-close or guaranteed memory-zeroization claim.
|
|
42
56
|
|
|
43
57
|
Verification uses real plaintext and Noise-encrypted local TCP fixtures plus the published native client's encoder/decoder and `MockTransport`. Real hardware behavior has not been verified in this lane.
|
|
44
58
|
|