homebridge-tasmota-discovery 1.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/.eslintrc.json +12 -0
- package/README.md +138 -0
- package/config.schema.json +64 -0
- package/dist/accessory.d.ts +61 -0
- package/dist/accessory.d.ts.map +1 -0
- package/dist/accessory.js +271 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/platform.d.ts +32 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +240 -0
- package/dist/settings.d.ts +3 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +6 -0
- package/dist/tasmota-types.d.ts +88 -0
- package/dist/tasmota-types.d.ts.map +1 -0
- package/dist/tasmota-types.js +3 -0
- package/package.json +40 -0
- package/src/accessory.ts +315 -0
- package/src/index.ts +7 -0
- package/src/platform.ts +238 -0
- package/src/settings.ts +2 -0
- package/src/tasmota-types.ts +88 -0
- package/tsconfig.json +18 -0
package/.eslintrc.json
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# homebridge-tasmota-discovery
|
|
2
|
+
|
|
3
|
+
> Automatic Tasmota device discovery for [Homebridge](https://homebridge.io) via MQTT — works like Zigbee2MQTT but for Tasmota devices.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/homebridge-tasmota-discovery)
|
|
6
|
+
[](https://github.com/homebridge/homebridge/wiki/Verified-Plugins)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## How it works
|
|
12
|
+
|
|
13
|
+
1. When Tasmota boots it publishes a **retained** discovery payload on `tasmota/discovery/{MAC}/config`.
|
|
14
|
+
2. This plugin subscribes to that topic and **auto-registers** every device in Apple Home — no manual config per device.
|
|
15
|
+
3. If the device IP changes (DHCP reassignment) the plugin detects it on the next boot and updates the accessory Serial Number automatically.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Requirements
|
|
20
|
+
|
|
21
|
+
| Dependency | Version |
|
|
22
|
+
|---|---|
|
|
23
|
+
| Homebridge | ≥ 2.0.0 |
|
|
24
|
+
| Node.js | ≥ 18.15.0 |
|
|
25
|
+
| Tasmota firmware | ≥ 9.1.0 (MQTT Discovery supported) |
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install -g homebridge-tasmota-discovery
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or install via the **Homebridge UI** (search for `homebridge-tasmota-discovery`).
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Tasmota device setup
|
|
40
|
+
|
|
41
|
+
MQTT Discovery must be enabled on each device. In the Tasmota console run:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
SetOption19 1
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Or navigate to **Configuration → MQTT** and make sure the **Discovery** option is enabled.
|
|
48
|
+
|
|
49
|
+
The device will then publish its discovery payload on `tasmota/discovery/{MAC}/config` (retained).
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Homebridge config
|
|
54
|
+
|
|
55
|
+
Add the platform block to your `config.json`:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"platforms": [
|
|
60
|
+
{
|
|
61
|
+
"platform": "TasmotaDiscovery",
|
|
62
|
+
"name": "TasmotaDiscovery",
|
|
63
|
+
"mqttHost": "192.168.1.10",
|
|
64
|
+
"mqttPort": 1883,
|
|
65
|
+
"mqttUsername": "myuser",
|
|
66
|
+
"mqttPassword": "mypassword"
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
| Key | Required | Default | Description |
|
|
73
|
+
|---|---|---|---|
|
|
74
|
+
| `mqttHost` | ✅ | — | MQTT broker hostname or IP |
|
|
75
|
+
| `mqttPort` | | 1883 | MQTT broker port |
|
|
76
|
+
| `mqttUsername` | | — | MQTT auth username |
|
|
77
|
+
| `mqttPassword` | | — | MQTT auth password |
|
|
78
|
+
| `mqttTls` | | false | Connect with TLS (port 8883) |
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Supported device types
|
|
83
|
+
|
|
84
|
+
| Tasmota `rl` value | HomeKit type |
|
|
85
|
+
|---|---|
|
|
86
|
+
| `1` — Relay/switch | **Switch** service |
|
|
87
|
+
| — `lt_st: 1` | **Lightbulb** (on/off + brightness) |
|
|
88
|
+
| — `lt_st: 2` | **Lightbulb** (on/off + brightness + color temperature) |
|
|
89
|
+
| — `lt_st: 3` | **Lightbulb** (on/off + brightness + hue + saturation) |
|
|
90
|
+
| — `lt_st: 4-5` | **Lightbulb** (full RGBW/RGBWW: brightness + CT + hue + saturation) |
|
|
91
|
+
|
|
92
|
+
Multi-relay devices (e.g. Sonoff 4CH) are represented as a **single accessory with multiple Switch services**, one per relay.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Accessory metadata
|
|
97
|
+
|
|
98
|
+
| HomeKit field | Source |
|
|
99
|
+
|---|---|
|
|
100
|
+
| **Manufacturer** | `"Tasmota"` (fixed) |
|
|
101
|
+
| **Model** | `md` field from discovery payload |
|
|
102
|
+
| **Serial Number** | Device IP address (`ip` field) |
|
|
103
|
+
| **Firmware** | Tasmota firmware version (`sw` field) |
|
|
104
|
+
| **Name** | Display name (`dn` field) |
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## MQTT topics used
|
|
109
|
+
|
|
110
|
+
| Topic | Direction | Purpose |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| `tasmota/discovery/+/config` | Subscribe | Device discovery (retained) |
|
|
113
|
+
| `tele/+/LWT` | Subscribe | Online / Offline status |
|
|
114
|
+
| `tele/+/STATE` | Subscribe | Periodic state updates |
|
|
115
|
+
| `stat/+/RESULT` | Subscribe | Command acknowledgements |
|
|
116
|
+
| `stat/+/POWER[N]` | Subscribe | Direct power state responses |
|
|
117
|
+
| `cmnd/{topic}/POWER[N]` | Publish | Switch on/off |
|
|
118
|
+
| `cmnd/{topic}/Dimmer` | Publish | Light brightness |
|
|
119
|
+
| `cmnd/{topic}/CT` | Publish | Light colour temperature |
|
|
120
|
+
| `cmnd/{topic}/HSBColor` | Publish | Light hue/saturation/brightness |
|
|
121
|
+
| `cmnd/{topic}/STATE` | Publish | Request current state on startup |
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Building from source
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
git clone https://github.com/youruser/homebridge-tasmota-discovery
|
|
129
|
+
cd homebridge-tasmota-discovery
|
|
130
|
+
npm install
|
|
131
|
+
npm run build
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
MIT © 2025
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pluginAlias": "TasmotaDiscovery",
|
|
3
|
+
"pluginType": "platform",
|
|
4
|
+
"singular": true,
|
|
5
|
+
"headerDisplay": "Automatically discovers Tasmota devices via MQTT and adds them to Apple Home. Devices are picked up from the `tasmota/discovery/+/config` retained messages published by Tasmota's MQTT Discovery feature.",
|
|
6
|
+
"footerDisplay": "Enable MQTT Discovery in your Tasmota devices: **Configuration → MQTT → SetOption19 1** (or `SetOption19 1` in the console).",
|
|
7
|
+
"schema": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"properties": {
|
|
10
|
+
"name": {
|
|
11
|
+
"title": "Platform Name",
|
|
12
|
+
"type": "string",
|
|
13
|
+
"default": "TasmotaDiscovery",
|
|
14
|
+
"required": true
|
|
15
|
+
},
|
|
16
|
+
"mqttHost": {
|
|
17
|
+
"title": "MQTT Broker Host",
|
|
18
|
+
"type": "string",
|
|
19
|
+
"required": true,
|
|
20
|
+
"description": "Hostname or IP address of your MQTT broker (e.g. 192.168.1.10 or localhost).",
|
|
21
|
+
"placeholder": "192.168.1.10"
|
|
22
|
+
},
|
|
23
|
+
"mqttPort": {
|
|
24
|
+
"title": "MQTT Broker Port",
|
|
25
|
+
"type": "integer",
|
|
26
|
+
"default": 1883,
|
|
27
|
+
"minimum": 1,
|
|
28
|
+
"maximum": 65535,
|
|
29
|
+
"description": "MQTT broker port. Default is 1883 (plain) or 8883 (TLS)."
|
|
30
|
+
},
|
|
31
|
+
"mqttUsername": {
|
|
32
|
+
"title": "MQTT Username",
|
|
33
|
+
"type": "string",
|
|
34
|
+
"description": "Optional MQTT broker username."
|
|
35
|
+
},
|
|
36
|
+
"mqttPassword": {
|
|
37
|
+
"title": "MQTT Password",
|
|
38
|
+
"type": "string",
|
|
39
|
+
"description": "Optional MQTT broker password."
|
|
40
|
+
},
|
|
41
|
+
"mqttTls": {
|
|
42
|
+
"title": "Use TLS (mqtts://)",
|
|
43
|
+
"type": "boolean",
|
|
44
|
+
"default": false,
|
|
45
|
+
"description": "Enable TLS for the MQTT connection."
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"required": ["mqttHost"]
|
|
49
|
+
},
|
|
50
|
+
"layout": [
|
|
51
|
+
{
|
|
52
|
+
"type": "fieldset",
|
|
53
|
+
"title": "MQTT Broker",
|
|
54
|
+
"items": ["mqttHost", "mqttPort", "mqttTls"]
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"type": "fieldset",
|
|
58
|
+
"title": "Authentication (optional)",
|
|
59
|
+
"expandable": true,
|
|
60
|
+
"expanded": false,
|
|
61
|
+
"items": ["mqttUsername", "mqttPassword"]
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { PlatformAccessory } from 'homebridge';
|
|
2
|
+
import { TasmotaDiscoveryPlatform } from './platform.js';
|
|
3
|
+
import { TasmotaDiscoveryConfig } from './tasmota-types.js';
|
|
4
|
+
/**
|
|
5
|
+
* TasmotaAccessory
|
|
6
|
+
*
|
|
7
|
+
* Wraps a single Tasmota device as a HomeKit accessory.
|
|
8
|
+
* Supports:
|
|
9
|
+
* - Multiple relay/switch channels (rl[i] === 1)
|
|
10
|
+
* - Single-channel light (lt_st === 1) with Brightness
|
|
11
|
+
* - Multi-channel lights (lt_st 2-5): On + Brightness + ColorTemperature/Hue/Saturation
|
|
12
|
+
*
|
|
13
|
+
* POWER commands:
|
|
14
|
+
* Single relay → cmnd/{t}/POWER
|
|
15
|
+
* Multi relay → cmnd/{t}/POWER{N} (1-based)
|
|
16
|
+
*/
|
|
17
|
+
export declare class TasmotaAccessory {
|
|
18
|
+
private readonly platform;
|
|
19
|
+
private readonly accessory;
|
|
20
|
+
/** Switch services keyed by 0-based relay index */
|
|
21
|
+
private switchServices;
|
|
22
|
+
/** The Lightbulb service (if device has a light) */
|
|
23
|
+
private lightService?;
|
|
24
|
+
/** Runtime power states keyed by 0-based relay index */
|
|
25
|
+
private relayStates;
|
|
26
|
+
/** 0-based indices of active relays */
|
|
27
|
+
private relayIndices;
|
|
28
|
+
private lightOn;
|
|
29
|
+
private lightBri;
|
|
30
|
+
private lightCT;
|
|
31
|
+
private lightHue;
|
|
32
|
+
private lightSat;
|
|
33
|
+
private config;
|
|
34
|
+
constructor(platform: TasmotaDiscoveryPlatform, accessory: PlatformAccessory, config: TasmotaDiscoveryConfig);
|
|
35
|
+
private setupAccessoryInfo;
|
|
36
|
+
private setupSwitchServices;
|
|
37
|
+
private relayFriendlyName;
|
|
38
|
+
private setRelay;
|
|
39
|
+
/**
|
|
40
|
+
* lt_st values:
|
|
41
|
+
* 1 = single channel (white / dimmable)
|
|
42
|
+
* 2 = cold + warm white
|
|
43
|
+
* 3 = RGB
|
|
44
|
+
* 4 = RGBW
|
|
45
|
+
* 5 = RGBWW (RGB + cold + warm white)
|
|
46
|
+
*/
|
|
47
|
+
private setupLightService;
|
|
48
|
+
private sendHSBColor;
|
|
49
|
+
/** Called when the device IP or firmware version changes */
|
|
50
|
+
updateConfig(newCfg: TasmotaDiscoveryConfig): void;
|
|
51
|
+
/** Update reachability status (LWT Online / Offline) */
|
|
52
|
+
setOnlineStatus(online: boolean): void;
|
|
53
|
+
/**
|
|
54
|
+
* Update switch/light state from any MQTT state message
|
|
55
|
+
* (tele/STATE, stat/RESULT, stat/POWER[N])
|
|
56
|
+
*/
|
|
57
|
+
updateState(state: Record<string, unknown>): void;
|
|
58
|
+
/** The Tasmota device topic (e.g. "sonoff-cabinet-compresor") */
|
|
59
|
+
getTopic(): string;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=accessory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accessory.d.ts","sourceRoot":"","sources":["../src/accessory.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,iBAAiB,EAElB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,EAAE,sBAAsB,EAAgB,MAAM,oBAAoB,CAAC;AAE1E;;;;;;;;;;;;GAYG;AACH,qBAAa,gBAAgB;IAoBzB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,SAAS;IApB5B,mDAAmD;IACnD,OAAO,CAAC,cAAc,CAAmC;IACzD,oDAAoD;IACpD,OAAO,CAAC,YAAY,CAAC,CAAU;IAE/B,wDAAwD;IACxD,OAAO,CAAC,WAAW,CAAmC;IACtD,uCAAuC;IACvC,OAAO,CAAC,YAAY,CAAgB;IAEpC,OAAO,CAAC,OAAO,CAAW;IAC1B,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAM;IACtB,OAAO,CAAC,QAAQ,CAAM;IAEtB,OAAO,CAAC,MAAM,CAAyB;gBAGpB,QAAQ,EAAE,wBAAwB,EAClC,SAAS,EAAE,iBAAiB,EAC7C,MAAM,EAAE,sBAAsB;IAYhC,OAAO,CAAC,kBAAkB;IAe1B,OAAO,CAAC,mBAAmB;IA+C3B,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,QAAQ;IAWhB;;;;;;;OAOG;IACH,OAAO,CAAC,iBAAiB;IAsDzB,OAAO,CAAC,YAAY;IAWpB,4DAA4D;IACrD,YAAY,CAAC,MAAM,EAAE,sBAAsB,GAAG,IAAI;IAwBzD,wDAAwD;IACjD,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI;IAkB7C;;;OAGG;IACI,WAAW,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAqDxD,iEAAiE;IAC1D,QAAQ,IAAI,MAAM;CAG1B"}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TasmotaAccessory = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* TasmotaAccessory
|
|
6
|
+
*
|
|
7
|
+
* Wraps a single Tasmota device as a HomeKit accessory.
|
|
8
|
+
* Supports:
|
|
9
|
+
* - Multiple relay/switch channels (rl[i] === 1)
|
|
10
|
+
* - Single-channel light (lt_st === 1) with Brightness
|
|
11
|
+
* - Multi-channel lights (lt_st 2-5): On + Brightness + ColorTemperature/Hue/Saturation
|
|
12
|
+
*
|
|
13
|
+
* POWER commands:
|
|
14
|
+
* Single relay → cmnd/{t}/POWER
|
|
15
|
+
* Multi relay → cmnd/{t}/POWER{N} (1-based)
|
|
16
|
+
*/
|
|
17
|
+
class TasmotaAccessory {
|
|
18
|
+
platform;
|
|
19
|
+
accessory;
|
|
20
|
+
/** Switch services keyed by 0-based relay index */
|
|
21
|
+
switchServices = new Map();
|
|
22
|
+
/** The Lightbulb service (if device has a light) */
|
|
23
|
+
lightService;
|
|
24
|
+
/** Runtime power states keyed by 0-based relay index */
|
|
25
|
+
relayStates = new Map();
|
|
26
|
+
/** 0-based indices of active relays */
|
|
27
|
+
relayIndices = [];
|
|
28
|
+
lightOn = false;
|
|
29
|
+
lightBri = 100; // 0-100
|
|
30
|
+
lightCT = 327; // mireds (warm white default)
|
|
31
|
+
lightHue = 0;
|
|
32
|
+
lightSat = 0;
|
|
33
|
+
config;
|
|
34
|
+
constructor(platform, accessory, config) {
|
|
35
|
+
this.platform = platform;
|
|
36
|
+
this.accessory = accessory;
|
|
37
|
+
this.config = config;
|
|
38
|
+
this.setupAccessoryInfo();
|
|
39
|
+
this.setupSwitchServices();
|
|
40
|
+
if (config.lt_st > 0) {
|
|
41
|
+
this.setupLightService();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// ─── Accessory Information ─────────────────────────────────────────────────
|
|
45
|
+
setupAccessoryInfo() {
|
|
46
|
+
const { Service: S, Characteristic: C } = this.platform;
|
|
47
|
+
const info = this.accessory.getService(S.AccessoryInformation)
|
|
48
|
+
?? this.accessory.addService(S.AccessoryInformation);
|
|
49
|
+
info
|
|
50
|
+
.setCharacteristic(C.Manufacturer, 'Tasmota')
|
|
51
|
+
.setCharacteristic(C.Model, this.config.md)
|
|
52
|
+
.setCharacteristic(C.SerialNumber, this.config.ip)
|
|
53
|
+
.setCharacteristic(C.FirmwareRevision, this.config.sw)
|
|
54
|
+
.setCharacteristic(C.Name, this.config.dn);
|
|
55
|
+
}
|
|
56
|
+
// ─── Switch Services ───────────────────────────────────────────────────────
|
|
57
|
+
setupSwitchServices() {
|
|
58
|
+
const { Service: S, Characteristic: C } = this.platform;
|
|
59
|
+
// Collect active relay indices (rl value 1 = relay)
|
|
60
|
+
this.relayIndices = this.config.rl
|
|
61
|
+
.map((v, i) => ({ v, i }))
|
|
62
|
+
.filter(({ v }) => v === 1)
|
|
63
|
+
.map(({ i }) => i);
|
|
64
|
+
const isSingle = this.relayIndices.length === 1;
|
|
65
|
+
for (const idx of this.relayIndices) {
|
|
66
|
+
const relayNum = idx + 1; // 1-based
|
|
67
|
+
const name = this.relayFriendlyName(idx);
|
|
68
|
+
const subtype = `relay_${relayNum}`;
|
|
69
|
+
let svc;
|
|
70
|
+
if (isSingle) {
|
|
71
|
+
svc = this.accessory.getService(S.Switch)
|
|
72
|
+
?? this.accessory.addService(S.Switch, name);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
svc = this.accessory.getServiceById(S.Switch, subtype)
|
|
76
|
+
?? this.accessory.addService(S.Switch, name, subtype);
|
|
77
|
+
}
|
|
78
|
+
svc.setCharacteristic(C.Name, name);
|
|
79
|
+
svc.getCharacteristic(C.On)
|
|
80
|
+
.onGet(() => this.relayStates.get(idx) ?? false)
|
|
81
|
+
.onSet((value) => this.setRelay(idx, value));
|
|
82
|
+
this.switchServices.set(idx, svc);
|
|
83
|
+
this.relayStates.set(idx, false);
|
|
84
|
+
}
|
|
85
|
+
// Remove stale Switch services from cache
|
|
86
|
+
this.accessory.services
|
|
87
|
+
.filter(s => s.UUID === S.Switch.UUID && s.subtype)
|
|
88
|
+
.forEach(s => {
|
|
89
|
+
const n = parseInt((s.subtype ?? '').replace('relay_', ''));
|
|
90
|
+
if (!this.relayIndices.includes(n - 1)) {
|
|
91
|
+
this.platform.log.debug(`[${this.config.dn}] Removing stale switch service: ${s.subtype}`);
|
|
92
|
+
this.accessory.removeService(s);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
relayFriendlyName(idx) {
|
|
97
|
+
const fn = this.config.fn[idx];
|
|
98
|
+
if (fn && fn.trim().length > 0)
|
|
99
|
+
return fn.trim();
|
|
100
|
+
if (this.relayIndices.length === 1)
|
|
101
|
+
return this.config.dn;
|
|
102
|
+
return `${this.config.dn} ${idx + 1}`;
|
|
103
|
+
}
|
|
104
|
+
setRelay(idx, on) {
|
|
105
|
+
const relayNum = idx + 1;
|
|
106
|
+
// Single relay: use "POWER"; multi: "POWER1", "POWER2", etc.
|
|
107
|
+
const cmd = this.relayIndices.length === 1 ? 'POWER' : `POWER${relayNum}`;
|
|
108
|
+
this.platform.log.info(`[${this.config.dn}] ${cmd} → ${on ? 'ON' : 'OFF'}`);
|
|
109
|
+
this.platform.publishCommand(this.config.t, cmd, on ? 'ON' : 'OFF');
|
|
110
|
+
this.relayStates.set(idx, on);
|
|
111
|
+
}
|
|
112
|
+
// ─── Light Service ─────────────────────────────────────────────────────────
|
|
113
|
+
/**
|
|
114
|
+
* lt_st values:
|
|
115
|
+
* 1 = single channel (white / dimmable)
|
|
116
|
+
* 2 = cold + warm white
|
|
117
|
+
* 3 = RGB
|
|
118
|
+
* 4 = RGBW
|
|
119
|
+
* 5 = RGBWW (RGB + cold + warm white)
|
|
120
|
+
*/
|
|
121
|
+
setupLightService() {
|
|
122
|
+
const { Service: S, Characteristic: C } = this.platform;
|
|
123
|
+
const name = `${this.config.dn} Light`;
|
|
124
|
+
this.lightService = this.accessory.getService(S.Lightbulb)
|
|
125
|
+
?? this.accessory.addService(S.Lightbulb, name, 'light');
|
|
126
|
+
this.lightService.setCharacteristic(C.Name, name);
|
|
127
|
+
// On/Off (always present)
|
|
128
|
+
this.lightService.getCharacteristic(C.On)
|
|
129
|
+
.onGet(() => this.lightOn)
|
|
130
|
+
.onSet((v) => {
|
|
131
|
+
this.lightOn = v;
|
|
132
|
+
this.platform.publishCommand(this.config.t, 'POWER', this.lightOn ? 'ON' : 'OFF');
|
|
133
|
+
});
|
|
134
|
+
// Brightness (all light types)
|
|
135
|
+
this.lightService.getCharacteristic(C.Brightness)
|
|
136
|
+
.onGet(() => this.lightBri)
|
|
137
|
+
.onSet((v) => {
|
|
138
|
+
this.lightBri = v;
|
|
139
|
+
this.platform.publishCommand(this.config.t, 'Dimmer', String(this.lightBri));
|
|
140
|
+
});
|
|
141
|
+
// Color Temperature (lt_st 2, 4, 5 — cold/warm channels)
|
|
142
|
+
if (this.config.lt_st === 2 || this.config.lt_st >= 4) {
|
|
143
|
+
this.lightService.getCharacteristic(C.ColorTemperature)
|
|
144
|
+
.setProps({ minValue: 153, maxValue: 500 }) // 153 = cool white, 500 = warm white
|
|
145
|
+
.onGet(() => this.lightCT)
|
|
146
|
+
.onSet((v) => {
|
|
147
|
+
this.lightCT = v;
|
|
148
|
+
this.platform.publishCommand(this.config.t, 'CT', String(this.lightCT));
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
// Hue + Saturation (lt_st 3, 4, 5 — has RGB channels)
|
|
152
|
+
if (this.config.lt_st >= 3) {
|
|
153
|
+
this.lightService.getCharacteristic(C.Hue)
|
|
154
|
+
.onGet(() => this.lightHue)
|
|
155
|
+
.onSet((v) => {
|
|
156
|
+
this.lightHue = v;
|
|
157
|
+
this.sendHSBColor();
|
|
158
|
+
});
|
|
159
|
+
this.lightService.getCharacteristic(C.Saturation)
|
|
160
|
+
.onGet(() => this.lightSat)
|
|
161
|
+
.onSet((v) => {
|
|
162
|
+
this.lightSat = v;
|
|
163
|
+
this.sendHSBColor();
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
sendHSBColor() {
|
|
168
|
+
// Tasmota accepts HSBColor as "hue,sat,bri"
|
|
169
|
+
this.platform.publishCommand(this.config.t, 'HSBColor', `${Math.round(this.lightHue)},${Math.round(this.lightSat)},${Math.round(this.lightBri)}`);
|
|
170
|
+
}
|
|
171
|
+
// ─── Public API (called by platform) ──────────────────────────────────────
|
|
172
|
+
/** Called when the device IP or firmware version changes */
|
|
173
|
+
updateConfig(newCfg) {
|
|
174
|
+
const { Service: S, Characteristic: C } = this.platform;
|
|
175
|
+
if (newCfg.ip !== this.config.ip) {
|
|
176
|
+
this.platform.log.info(`[${this.config.dn}] IP changed: ${this.config.ip} → ${newCfg.ip}`);
|
|
177
|
+
this.accessory.getService(S.AccessoryInformation)
|
|
178
|
+
?.setCharacteristic(C.SerialNumber, newCfg.ip);
|
|
179
|
+
}
|
|
180
|
+
if (newCfg.sw !== this.config.sw) {
|
|
181
|
+
this.accessory.getService(S.AccessoryInformation)
|
|
182
|
+
?.setCharacteristic(C.FirmwareRevision, newCfg.sw);
|
|
183
|
+
}
|
|
184
|
+
if (newCfg.md !== this.config.md) {
|
|
185
|
+
this.accessory.getService(S.AccessoryInformation)
|
|
186
|
+
?.setCharacteristic(C.Model, newCfg.md);
|
|
187
|
+
}
|
|
188
|
+
this.config = newCfg;
|
|
189
|
+
}
|
|
190
|
+
/** Update reachability status (LWT Online / Offline) */
|
|
191
|
+
setOnlineStatus(online) {
|
|
192
|
+
this.platform.log.info(`[${this.config.dn}] ${online ? 'Online' : 'Offline'}`);
|
|
193
|
+
// Set StatusFault on all Switch services to indicate device offline
|
|
194
|
+
const { Characteristic: C } = this.platform;
|
|
195
|
+
const fault = online
|
|
196
|
+
? C.StatusFault.NO_FAULT
|
|
197
|
+
: C.StatusFault.GENERAL_FAULT;
|
|
198
|
+
for (const svc of this.switchServices.values()) {
|
|
199
|
+
if (svc.testCharacteristic(C.StatusFault)) {
|
|
200
|
+
svc.getCharacteristic(C.StatusFault).updateValue(fault);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (this.lightService?.testCharacteristic(C.StatusFault)) {
|
|
204
|
+
this.lightService.getCharacteristic(C.StatusFault).updateValue(fault);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Update switch/light state from any MQTT state message
|
|
209
|
+
* (tele/STATE, stat/RESULT, stat/POWER[N])
|
|
210
|
+
*/
|
|
211
|
+
updateState(state) {
|
|
212
|
+
const { Characteristic: C } = this.platform;
|
|
213
|
+
// ── Relay states ─────────────────────────────────────────────
|
|
214
|
+
for (const idx of this.relayIndices) {
|
|
215
|
+
const relayNum = idx + 1;
|
|
216
|
+
// Single relay may report POWER or POWER1
|
|
217
|
+
const keys = this.relayIndices.length === 1
|
|
218
|
+
? ['POWER', 'POWER1']
|
|
219
|
+
: [`POWER${relayNum}`];
|
|
220
|
+
for (const key of keys) {
|
|
221
|
+
if (key in state) {
|
|
222
|
+
const on = state[key] === 'ON';
|
|
223
|
+
this.relayStates.set(idx, on);
|
|
224
|
+
this.switchServices.get(idx)
|
|
225
|
+
?.getCharacteristic(C.On)
|
|
226
|
+
.updateValue(on);
|
|
227
|
+
this.platform.log.debug(`[${this.config.dn}] ${key} = ${on ? 'ON' : 'OFF'}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// ── Light state (only if device has a light) ──────────────────
|
|
232
|
+
if (this.lightService && this.config.lt_st > 0) {
|
|
233
|
+
const s = state;
|
|
234
|
+
if ('POWER' in s || 'POWER1' in s) {
|
|
235
|
+
this.lightOn = (s.POWER ?? s.POWER1) === 'ON';
|
|
236
|
+
this.lightService.getCharacteristic(C.On).updateValue(this.lightOn);
|
|
237
|
+
}
|
|
238
|
+
if (typeof s.Dimmer === 'number') {
|
|
239
|
+
this.lightBri = s.Dimmer;
|
|
240
|
+
this.lightService.getCharacteristic(C.Brightness).updateValue(this.lightBri);
|
|
241
|
+
}
|
|
242
|
+
if (typeof s.CT === 'number') {
|
|
243
|
+
this.lightCT = s.CT;
|
|
244
|
+
if (this.lightService.testCharacteristic(C.ColorTemperature)) {
|
|
245
|
+
this.lightService.getCharacteristic(C.ColorTemperature).updateValue(this.lightCT);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (typeof s.HSBColor === 'string') {
|
|
249
|
+
const [h, sat, bri] = s.HSBColor.split(',').map(Number);
|
|
250
|
+
if (!isNaN(h)) {
|
|
251
|
+
this.lightHue = h;
|
|
252
|
+
this.lightService.getCharacteristic(C.Hue).updateValue(h);
|
|
253
|
+
}
|
|
254
|
+
if (!isNaN(sat)) {
|
|
255
|
+
this.lightSat = sat;
|
|
256
|
+
this.lightService.getCharacteristic(C.Saturation).updateValue(sat);
|
|
257
|
+
}
|
|
258
|
+
if (!isNaN(bri)) {
|
|
259
|
+
this.lightBri = bri;
|
|
260
|
+
this.lightService.getCharacteristic(C.Brightness).updateValue(bri);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/** The Tasmota device topic (e.g. "sonoff-cabinet-compresor") */
|
|
266
|
+
getTopic() {
|
|
267
|
+
return this.config.t;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
exports.TasmotaAccessory = TasmotaAccessory;
|
|
271
|
+
//# sourceMappingURL=accessory.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;yBAIvB,KAAK,GAAG,KAAG,IAAI;AAAzB,kBAEE"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const platform_js_1 = require("./platform.js");
|
|
3
|
+
const settings_js_1 = require("./settings.js");
|
|
4
|
+
module.exports = (api) => {
|
|
5
|
+
api.registerPlatform(settings_js_1.PLUGIN_NAME, settings_js_1.PLATFORM_NAME, platform_js_1.TasmotaDiscoveryPlatform);
|
|
6
|
+
};
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { API, Characteristic, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig, Service } from 'homebridge';
|
|
2
|
+
import { TasmotaAccessory } from './accessory.js';
|
|
3
|
+
export declare class TasmotaDiscoveryPlatform implements DynamicPlatformPlugin {
|
|
4
|
+
readonly log: Logger;
|
|
5
|
+
readonly config: PlatformConfig;
|
|
6
|
+
readonly api: API;
|
|
7
|
+
readonly Service: typeof Service;
|
|
8
|
+
readonly Characteristic: typeof Characteristic;
|
|
9
|
+
/** Accessories restored from Homebridge cache, keyed by UUID */
|
|
10
|
+
readonly cachedAccessories: Map<string, PlatformAccessory>;
|
|
11
|
+
/** Live accessory handlers, keyed by MAC address (uppercase) */
|
|
12
|
+
readonly tasmotaAccessories: Map<string, TasmotaAccessory>;
|
|
13
|
+
private mqttClient?;
|
|
14
|
+
constructor(log: Logger, config: PlatformConfig, api: API);
|
|
15
|
+
/**
|
|
16
|
+
* Called by Homebridge for every accessory in the persistent cache.
|
|
17
|
+
* We store them so we can restore them when a discovery message arrives.
|
|
18
|
+
*/
|
|
19
|
+
configureAccessory(accessory: PlatformAccessory): void;
|
|
20
|
+
private connectMqtt;
|
|
21
|
+
private handleMessage;
|
|
22
|
+
private handleDiscovery;
|
|
23
|
+
private findByTopic;
|
|
24
|
+
/**
|
|
25
|
+
* Publish a command to a Tasmota device.
|
|
26
|
+
* topic = device topic (e.g. "sonoff-cabinet-compresor")
|
|
27
|
+
* cmd = command name (e.g. "POWER", "POWER2", "STATE")
|
|
28
|
+
* value = payload (e.g. "ON", "OFF", "" for query)
|
|
29
|
+
*/
|
|
30
|
+
publishCommand(topic: string, cmd: string, value: string): void;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=platform.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,GAAG,EACH,cAAc,EACd,qBAAqB,EACrB,MAAM,EACN,iBAAiB,EACjB,cAAc,EACd,OAAO,EACR,MAAM,YAAY,CAAC;AAIpB,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAGlD,qBAAa,wBAAyB,YAAW,qBAAqB;aAalD,GAAG,EAAE,MAAM;aACX,MAAM,EAAE,cAAc;aACtB,GAAG,EAAE,GAAG;IAd1B,SAAgB,OAAO,EAAE,OAAO,OAAO,CAAC;IACxC,SAAgB,cAAc,EAAE,OAAO,cAAc,CAAC;IAEtD,gEAAgE;IAChE,SAAgB,iBAAiB,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAa;IAE9E,gEAAgE;IAChE,SAAgB,kBAAkB,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAa;IAE9E,OAAO,CAAC,UAAU,CAAC,CAAkB;gBAGnB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,GAAG;IAqB1B;;;OAGG;IACH,kBAAkB,CAAC,SAAS,EAAE,iBAAiB,GAAG,IAAI;IAOtD,OAAO,CAAC,WAAW;IA4DnB,OAAO,CAAC,aAAa;IAiDrB,OAAO,CAAC,eAAe;IA6CvB,OAAO,CAAC,WAAW;IAOnB;;;;;OAKG;IACI,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;CAQvE"}
|