ildevice-homeassistant 0.2.0__tar.gz
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.
- ildevice_homeassistant-0.2.0/.gitignore +4 -0
- ildevice_homeassistant-0.2.0/LICENSE +21 -0
- ildevice_homeassistant-0.2.0/PKG-INFO +197 -0
- ildevice_homeassistant-0.2.0/README.md +185 -0
- ildevice_homeassistant-0.2.0/ildevice_ha/__init__.py +8 -0
- ildevice_homeassistant-0.2.0/ildevice_ha/attach.py +50 -0
- ildevice_homeassistant-0.2.0/ildevice_ha/const.py +50 -0
- ildevice_homeassistant-0.2.0/ildevice_ha/core/__init__.py +35 -0
- ildevice_homeassistant-0.2.0/ildevice_ha/core/descriptor.py +182 -0
- ildevice_homeassistant-0.2.0/ildevice_ha/core/icons.py +183 -0
- ildevice_homeassistant-0.2.0/ildevice_ha/core/memory.py +71 -0
- ildevice_homeassistant-0.2.0/ildevice_ha/core/model.py +322 -0
- ildevice_homeassistant-0.2.0/ildevice_ha/core/plan.py +282 -0
- ildevice_homeassistant-0.2.0/ildevice_ha/core/topics.py +75 -0
- ildevice_homeassistant-0.2.0/ildevice_ha/core/transport.py +36 -0
- ildevice_homeassistant-0.2.0/ildevice_ha/core/values.py +64 -0
- ildevice_homeassistant-0.2.0/ildevice_ha/entity.py +796 -0
- ildevice_homeassistant-0.2.0/ildevice_ha/hub.py +177 -0
- ildevice_homeassistant-0.2.0/ildevice_ha/mqtt_transport.py +29 -0
- ildevice_homeassistant-0.2.0/pyproject.toml +34 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 3735943886
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ildevice-homeassistant
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: The IL device consumer for Home Assistant: IL descriptors become entities
|
|
5
|
+
Project-URL: Source, https://github.com/3735943886/ildevice-homeassistant
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Provides-Extra: test
|
|
10
|
+
Requires-Dist: pytest-homeassistant-custom-component==0.13.366; extra == 'test'
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
# IL device for Home Assistant
|
|
16
|
+
|
|
17
|
+
A Home Assistant integration for [ildevice](https://github.com/3735943886/ildevice) (Intermediate
|
|
18
|
+
Layer device model). It turns the ildevice descriptors that producers publish over MQTT into
|
|
19
|
+
Home Assistant devices and entities. It knows nothing about any one producer or model: a device
|
|
20
|
+
from rusthinq, rustuya or anything else that publishes a descriptor gets its device and entities
|
|
21
|
+
from the descriptor alone.
|
|
22
|
+
|
|
23
|
+
## Setup
|
|
24
|
+
|
|
25
|
+
Requires Home Assistant's `mqtt` integration. Install **IL device** through HACS (or copy
|
|
26
|
+
`custom_components/ildevice`), restart, and add the integration. Add it again for each topic
|
|
27
|
+
prefix you use (see [Several hubs](#several-hubs)).
|
|
28
|
+
|
|
29
|
+
| option | meaning |
|
|
30
|
+
|---|---|
|
|
31
|
+
| Topic prefix | The ildevice prefix the producers publish under (default `il`), for example `il/tuya`. Each entry has one prefix, so it must be unique. |
|
|
32
|
+
| Seconds before a device is shown unavailable | Keeps a short outage from showing (default `0`). |
|
|
33
|
+
| Legacy entity ids | JSON aliases that keep the unique ids of an earlier integration, see below. |
|
|
34
|
+
| Add discovered devices automatically | Skip the add / ignore question, see below. |
|
|
35
|
+
|
|
36
|
+
For rusthinq scripts that publish `<prefix>/<id>/descriptor`, set the integration
|
|
37
|
+
Topic prefix to the host's `[drivers] topic_prefix` (for example `rusthinq`).
|
|
38
|
+
Existing `<prefix>/<id>` descriptors remain supported. Property and command
|
|
39
|
+
paths come from the descriptor's `x-mqtt` block when provided.
|
|
40
|
+
|
|
41
|
+
## Several hubs
|
|
42
|
+
|
|
43
|
+
Add the integration once per topic prefix, such as `il/tuya` or `il/thinq`: each entry is its own hub
|
|
44
|
+
with its own options (auto-add, offline grace, legacy ids) and shows up in the integration's list under
|
|
45
|
+
its prefix, with its devices beneath it. A discovered device is offered to, and added to, the entry it
|
|
46
|
+
was found on. Changing an entry's prefix renames it. A prefix can be used by one entry only. Device ids
|
|
47
|
+
should be unique across entries, as they are the device identifier and the base of the unique ids.
|
|
48
|
+
|
|
49
|
+
## How it works
|
|
50
|
+
|
|
51
|
+
[docs/examples.md](docs/examples.md) walks through a plug, a light, an air conditioner, a curtain
|
|
52
|
+
with a light, a button and a valve on its own topics: what the producer publishes, the entities
|
|
53
|
+
it becomes, and the MQTT messages for commands.
|
|
54
|
+
|
|
55
|
+
- It subscribes to `<il_prefix>/+` and `<il_prefix>/+/descriptor` (retained descriptors), and `<il_prefix>/_producer/+`
|
|
56
|
+
(producer presence). For each device it subscribes to the value topics and the `reject` topic the
|
|
57
|
+
descriptor points at (`x-mqtt`, see `il-mqtt.md` in the ildevice repository).
|
|
58
|
+
- A device's **kind** and the **roles** of its properties decide the composite entities. A group
|
|
59
|
+
with its own `kind` is a composite of its own, so one device can be a light and a cover.
|
|
60
|
+
|
|
61
|
+
| kind | entity | it needs |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `climate` | `climate` | `target_temperature` |
|
|
64
|
+
| `humidifier` | `humidifier` (class `dehumidifier` for a dehumidifier) | `on`, `target_humidity` |
|
|
65
|
+
| `fan` | `fan` (`fan_speed`, or `speed` when the device takes a percentage; `oscillate`, `direction`) | `on` |
|
|
66
|
+
| `light` | `light`; modes follow from `brightness`, `color_temperature`, `color` | `on` |
|
|
67
|
+
| `cover` | `cover` | `position`, `open` or `close` |
|
|
68
|
+
| `lock` | `lock` | writable `locked` |
|
|
69
|
+
| `valve` | `valve` | writable `opened` |
|
|
70
|
+
| `siren` | `siren` | `on` |
|
|
71
|
+
| `alarm` | `alarm_control_panel` | see `il.md` |
|
|
72
|
+
| `vacuum` | `vacuum` | see `il.md` |
|
|
73
|
+
|
|
74
|
+
A cover without `open` / `close` moves by writing `position` 100 / 0. A cover is closed when its
|
|
75
|
+
`position` is 0, or else when its `cover_state` is `closed`; `cover_state` also says opening and
|
|
76
|
+
closing. A lock shows `lock_state` (locking, unlocking, jammed, open) when it has one. A lock or valve that is not
|
|
77
|
+
writable stays a plain binary sensor: a role never implies control.
|
|
78
|
+
- Every other property is a plain entity chosen by its type: `sensor` / `binary_sensor` /
|
|
79
|
+
`switch` / `number` / `select` / `text` / `button` / `event`. `class`, `series` and `category`
|
|
80
|
+
become `device_class`, `state_class` and `entity_category`; a property with no device class gets
|
|
81
|
+
an icon chosen from its name, unit and kind.
|
|
82
|
+
- A control whose property has `requires` (a binary that must be true, or a select whose value must
|
|
83
|
+
be in a list) is unavailable, or for one control of a composite refused, until the condition
|
|
84
|
+
holds. A refused command arrives as an `ildevice_command_rejected` event (`device_id`, `prop`,
|
|
85
|
+
`reason`).
|
|
86
|
+
- Availability is the device's `available` property and the producer's presence
|
|
87
|
+
(`<il_prefix>/_producer/<source>`; `offline` makes all of its devices unavailable).
|
|
88
|
+
`offline_grace` delays unavailability reported by the device; producer-offline presence takes effect immediately.
|
|
89
|
+
- An empty MQTT descriptor payload withdraws the device: its entities, registry entries and value
|
|
90
|
+
subscriptions go, and it is created again if the descriptor is published again. A changed
|
|
91
|
+
descriptor re-plans its entities.
|
|
92
|
+
- Unknown fields are ignored. An unknown role falls back to a plain entity; a property with an unknown type is skipped.
|
|
93
|
+
|
|
94
|
+
Commands have no correlated success acknowledgement or completion deadline. State updates
|
|
95
|
+
show observed state; a successful trigger or unchanged write can be silent. A rejection
|
|
96
|
+
reports failure, and silence does not confirm success. Retained replay restores state per
|
|
97
|
+
topic and is not an atomic snapshot across the device's properties.
|
|
98
|
+
|
|
99
|
+
## Adding devices
|
|
100
|
+
|
|
101
|
+
A device that publishes a descriptor is not created at once. Home Assistant shows it under
|
|
102
|
+
"Discovered" (named after the descriptor's `label`), and you add it or ignore it. An ignored device
|
|
103
|
+
is not offered again. Deleting a device in Home Assistant makes it a new offer the next time it
|
|
104
|
+
appears. The option "Add discovered devices automatically" skips the question and creates every
|
|
105
|
+
device, and turning it off keeps the devices it had added.
|
|
106
|
+
|
|
107
|
+
## Keeping the ids of an earlier integration
|
|
108
|
+
|
|
109
|
+
An entity's unique id is `<device id>-<key>`, where the key is the property name, or for a
|
|
110
|
+
composite its platform (`climate`, `light`, ...) or its group name. To keep the ids an earlier
|
|
111
|
+
integration created, give aliases in the options, as JSON keyed by model (`*` for any):
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{"DHUM_056905_WW": {"uvnano": "uv_nano", "tank_full": "bucket_full"}, "*": {}}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Compared with MQTT discovery
|
|
118
|
+
|
|
119
|
+
Home Assistant's own MQTT discovery also builds devices from retained MQTT messages. It has no
|
|
120
|
+
setup, and any producer that follows it works with nothing extra installed. This integration
|
|
121
|
+
needs a producer that publishes ildevice descriptors, and in return:
|
|
122
|
+
|
|
123
|
+
- **One descriptor per device, not one config per entity.** The producer says what each property
|
|
124
|
+
is (its role, type and unit), not which Home Assistant entity to build. The same descriptor
|
|
125
|
+
serves any other ildevice consumer.
|
|
126
|
+
- **Devices made of several parts.** Roles become climate, humidifier, fan, light, cover, lock,
|
|
127
|
+
valve, siren, alarm and vacuum entities. Groups let one device hold several of them, such as a
|
|
128
|
+
light and a cover, next to its plain sensors and switches.
|
|
129
|
+
- **Separate hubs per topic prefix.** MQTT discovery has one discovery prefix for everything.
|
|
130
|
+
Here `il/tuya` and `il/thinq` can be separate entries, each with its own auto-add, offline grace
|
|
131
|
+
and alias options, and each lists its own devices.
|
|
132
|
+
- **You choose which devices to add.** A discovered device is offered to add or ignore, rather
|
|
133
|
+
than created at once.
|
|
134
|
+
- **Refused commands are reported.** A producer can refuse a command on the device's `reject`
|
|
135
|
+
topic, and Home Assistant fires `ildevice_command_rejected` with the reason. A control whose
|
|
136
|
+
`requires` condition does not hold is unavailable, rather than sending a command that will fail.
|
|
137
|
+
- **Availability comes from the producer too.** When a producer goes offline, all of its devices
|
|
138
|
+
go unavailable. The offline grace keeps a short outage from showing.
|
|
139
|
+
- **Unique ids can be kept.** Aliases map entity keys to the ones that an earlier integration
|
|
140
|
+
used (see [Keeping the ids](#keeping-the-ids-of-an-earlier-integration)).
|
|
141
|
+
- **It also runs without a broker.** Another integration can embed the consumer with an in-process
|
|
142
|
+
transport (see [Embedding](#embedding)).
|
|
143
|
+
|
|
144
|
+
## Layout
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
ildevice_ha/core/ Home Assistant free: descriptor, plan (entities), topics, values,
|
|
148
|
+
the consumer model, transport interface, in-process broker, icons
|
|
149
|
+
ildevice_ha/ the Home Assistant layer: hub (the model's sink), entity classes,
|
|
150
|
+
HA mqtt transport, attach; published to PyPI as `ildevice-homeassistant`
|
|
151
|
+
custom_components/ildevice/ the integration: config flow and platforms, requiring `ildevice-homeassistant`
|
|
152
|
+
tests/ core and model tests, the ildevice vectors, and the integration
|
|
153
|
+
in a test Home Assistant
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`core/plan.py` is where a descriptor becomes entities; nothing in it looks at a model.
|
|
157
|
+
`core/model.py` follows descriptors, values, presence and rejections on a `Transport` and tells a
|
|
158
|
+
`Sink` what changed, so the same core runs without Home Assistant.
|
|
159
|
+
|
|
160
|
+
### Embedding
|
|
161
|
+
|
|
162
|
+
Another integration that produces ildevice descriptors in the same process can use the consumer
|
|
163
|
+
instead of going through a broker. It lists `ildevice-homeassistant` in its manifest
|
|
164
|
+
`requirements` (the same pinned version as this integration's manifest), not this integration in
|
|
165
|
+
`dependencies`, and calls `ildevice_ha.attach.build_hub(hass, options, transport, platform)` with
|
|
166
|
+
its own transport (for example `core.memory.InProcessTransport`, an in-process broker with MQTT's
|
|
167
|
+
filters and retained messages) and its own domain. It forwards the platforms listed in
|
|
168
|
+
`const.PLATFORMS` to its config entry and hands each one's `async_add_entities` to
|
|
169
|
+
`hub.register_platform`.
|
|
170
|
+
|
|
171
|
+
`InProcessTransport` keeps retained values but does not keep a publish history by default.
|
|
172
|
+
Tests that inspect `transport.published` should construct it with `record_published=True`.
|
|
173
|
+
|
|
174
|
+
## Specification baseline
|
|
175
|
+
|
|
176
|
+
The MQTT consumer is tested against the conformance vectors of ildevice specification **v0.1.0** (draft `il: 0`),
|
|
177
|
+
commit [bc51a9e](https://github.com/3735943886/ildevice/tree/bc51a9ef8ac46770f67a02722c5c92907d1019ee).
|
|
178
|
+
CI pins that revision because draft changes can break compatibility without changing `il`.
|
|
179
|
+
Local vector tests use the checkout selected by `ILDEVICE` (or `../ildevice`), so they can
|
|
180
|
+
also check proposed specification changes before the baseline is advanced.
|
|
181
|
+
|
|
182
|
+
## Tests
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
python3.14 -m venv .venv
|
|
186
|
+
.venv/bin/python -m pip install -e ".[test]"
|
|
187
|
+
.venv/bin/python -m pytest
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
- `tests/fixtures/rusthinq/` holds the descriptors the rusthinq drivers publish (ten LG
|
|
191
|
+
appliances), which cover the composite kinds, `trigger` / `requires` and groups.
|
|
192
|
+
- `tests/fixtures/sample/` holds descriptors from a Tuya producer with its own topic layout: a
|
|
193
|
+
plug, bulb, curtain, lock, valve, siren and button.
|
|
194
|
+
- `tests/test_vectors.py` runs the language-neutral conformance vectors of the ildevice
|
|
195
|
+
repository. It looks for a checkout at `../ildevice`, or at `$ILDEVICE`, and is skipped without
|
|
196
|
+
one.
|
|
197
|
+
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# IL device for Home Assistant
|
|
4
|
+
|
|
5
|
+
A Home Assistant integration for [ildevice](https://github.com/3735943886/ildevice) (Intermediate
|
|
6
|
+
Layer device model). It turns the ildevice descriptors that producers publish over MQTT into
|
|
7
|
+
Home Assistant devices and entities. It knows nothing about any one producer or model: a device
|
|
8
|
+
from rusthinq, rustuya or anything else that publishes a descriptor gets its device and entities
|
|
9
|
+
from the descriptor alone.
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
Requires Home Assistant's `mqtt` integration. Install **IL device** through HACS (or copy
|
|
14
|
+
`custom_components/ildevice`), restart, and add the integration. Add it again for each topic
|
|
15
|
+
prefix you use (see [Several hubs](#several-hubs)).
|
|
16
|
+
|
|
17
|
+
| option | meaning |
|
|
18
|
+
|---|---|
|
|
19
|
+
| Topic prefix | The ildevice prefix the producers publish under (default `il`), for example `il/tuya`. Each entry has one prefix, so it must be unique. |
|
|
20
|
+
| Seconds before a device is shown unavailable | Keeps a short outage from showing (default `0`). |
|
|
21
|
+
| Legacy entity ids | JSON aliases that keep the unique ids of an earlier integration, see below. |
|
|
22
|
+
| Add discovered devices automatically | Skip the add / ignore question, see below. |
|
|
23
|
+
|
|
24
|
+
For rusthinq scripts that publish `<prefix>/<id>/descriptor`, set the integration
|
|
25
|
+
Topic prefix to the host's `[drivers] topic_prefix` (for example `rusthinq`).
|
|
26
|
+
Existing `<prefix>/<id>` descriptors remain supported. Property and command
|
|
27
|
+
paths come from the descriptor's `x-mqtt` block when provided.
|
|
28
|
+
|
|
29
|
+
## Several hubs
|
|
30
|
+
|
|
31
|
+
Add the integration once per topic prefix, such as `il/tuya` or `il/thinq`: each entry is its own hub
|
|
32
|
+
with its own options (auto-add, offline grace, legacy ids) and shows up in the integration's list under
|
|
33
|
+
its prefix, with its devices beneath it. A discovered device is offered to, and added to, the entry it
|
|
34
|
+
was found on. Changing an entry's prefix renames it. A prefix can be used by one entry only. Device ids
|
|
35
|
+
should be unique across entries, as they are the device identifier and the base of the unique ids.
|
|
36
|
+
|
|
37
|
+
## How it works
|
|
38
|
+
|
|
39
|
+
[docs/examples.md](docs/examples.md) walks through a plug, a light, an air conditioner, a curtain
|
|
40
|
+
with a light, a button and a valve on its own topics: what the producer publishes, the entities
|
|
41
|
+
it becomes, and the MQTT messages for commands.
|
|
42
|
+
|
|
43
|
+
- It subscribes to `<il_prefix>/+` and `<il_prefix>/+/descriptor` (retained descriptors), and `<il_prefix>/_producer/+`
|
|
44
|
+
(producer presence). For each device it subscribes to the value topics and the `reject` topic the
|
|
45
|
+
descriptor points at (`x-mqtt`, see `il-mqtt.md` in the ildevice repository).
|
|
46
|
+
- A device's **kind** and the **roles** of its properties decide the composite entities. A group
|
|
47
|
+
with its own `kind` is a composite of its own, so one device can be a light and a cover.
|
|
48
|
+
|
|
49
|
+
| kind | entity | it needs |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `climate` | `climate` | `target_temperature` |
|
|
52
|
+
| `humidifier` | `humidifier` (class `dehumidifier` for a dehumidifier) | `on`, `target_humidity` |
|
|
53
|
+
| `fan` | `fan` (`fan_speed`, or `speed` when the device takes a percentage; `oscillate`, `direction`) | `on` |
|
|
54
|
+
| `light` | `light`; modes follow from `brightness`, `color_temperature`, `color` | `on` |
|
|
55
|
+
| `cover` | `cover` | `position`, `open` or `close` |
|
|
56
|
+
| `lock` | `lock` | writable `locked` |
|
|
57
|
+
| `valve` | `valve` | writable `opened` |
|
|
58
|
+
| `siren` | `siren` | `on` |
|
|
59
|
+
| `alarm` | `alarm_control_panel` | see `il.md` |
|
|
60
|
+
| `vacuum` | `vacuum` | see `il.md` |
|
|
61
|
+
|
|
62
|
+
A cover without `open` / `close` moves by writing `position` 100 / 0. A cover is closed when its
|
|
63
|
+
`position` is 0, or else when its `cover_state` is `closed`; `cover_state` also says opening and
|
|
64
|
+
closing. A lock shows `lock_state` (locking, unlocking, jammed, open) when it has one. A lock or valve that is not
|
|
65
|
+
writable stays a plain binary sensor: a role never implies control.
|
|
66
|
+
- Every other property is a plain entity chosen by its type: `sensor` / `binary_sensor` /
|
|
67
|
+
`switch` / `number` / `select` / `text` / `button` / `event`. `class`, `series` and `category`
|
|
68
|
+
become `device_class`, `state_class` and `entity_category`; a property with no device class gets
|
|
69
|
+
an icon chosen from its name, unit and kind.
|
|
70
|
+
- A control whose property has `requires` (a binary that must be true, or a select whose value must
|
|
71
|
+
be in a list) is unavailable, or for one control of a composite refused, until the condition
|
|
72
|
+
holds. A refused command arrives as an `ildevice_command_rejected` event (`device_id`, `prop`,
|
|
73
|
+
`reason`).
|
|
74
|
+
- Availability is the device's `available` property and the producer's presence
|
|
75
|
+
(`<il_prefix>/_producer/<source>`; `offline` makes all of its devices unavailable).
|
|
76
|
+
`offline_grace` delays unavailability reported by the device; producer-offline presence takes effect immediately.
|
|
77
|
+
- An empty MQTT descriptor payload withdraws the device: its entities, registry entries and value
|
|
78
|
+
subscriptions go, and it is created again if the descriptor is published again. A changed
|
|
79
|
+
descriptor re-plans its entities.
|
|
80
|
+
- Unknown fields are ignored. An unknown role falls back to a plain entity; a property with an unknown type is skipped.
|
|
81
|
+
|
|
82
|
+
Commands have no correlated success acknowledgement or completion deadline. State updates
|
|
83
|
+
show observed state; a successful trigger or unchanged write can be silent. A rejection
|
|
84
|
+
reports failure, and silence does not confirm success. Retained replay restores state per
|
|
85
|
+
topic and is not an atomic snapshot across the device's properties.
|
|
86
|
+
|
|
87
|
+
## Adding devices
|
|
88
|
+
|
|
89
|
+
A device that publishes a descriptor is not created at once. Home Assistant shows it under
|
|
90
|
+
"Discovered" (named after the descriptor's `label`), and you add it or ignore it. An ignored device
|
|
91
|
+
is not offered again. Deleting a device in Home Assistant makes it a new offer the next time it
|
|
92
|
+
appears. The option "Add discovered devices automatically" skips the question and creates every
|
|
93
|
+
device, and turning it off keeps the devices it had added.
|
|
94
|
+
|
|
95
|
+
## Keeping the ids of an earlier integration
|
|
96
|
+
|
|
97
|
+
An entity's unique id is `<device id>-<key>`, where the key is the property name, or for a
|
|
98
|
+
composite its platform (`climate`, `light`, ...) or its group name. To keep the ids an earlier
|
|
99
|
+
integration created, give aliases in the options, as JSON keyed by model (`*` for any):
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{"DHUM_056905_WW": {"uvnano": "uv_nano", "tank_full": "bucket_full"}, "*": {}}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Compared with MQTT discovery
|
|
106
|
+
|
|
107
|
+
Home Assistant's own MQTT discovery also builds devices from retained MQTT messages. It has no
|
|
108
|
+
setup, and any producer that follows it works with nothing extra installed. This integration
|
|
109
|
+
needs a producer that publishes ildevice descriptors, and in return:
|
|
110
|
+
|
|
111
|
+
- **One descriptor per device, not one config per entity.** The producer says what each property
|
|
112
|
+
is (its role, type and unit), not which Home Assistant entity to build. The same descriptor
|
|
113
|
+
serves any other ildevice consumer.
|
|
114
|
+
- **Devices made of several parts.** Roles become climate, humidifier, fan, light, cover, lock,
|
|
115
|
+
valve, siren, alarm and vacuum entities. Groups let one device hold several of them, such as a
|
|
116
|
+
light and a cover, next to its plain sensors and switches.
|
|
117
|
+
- **Separate hubs per topic prefix.** MQTT discovery has one discovery prefix for everything.
|
|
118
|
+
Here `il/tuya` and `il/thinq` can be separate entries, each with its own auto-add, offline grace
|
|
119
|
+
and alias options, and each lists its own devices.
|
|
120
|
+
- **You choose which devices to add.** A discovered device is offered to add or ignore, rather
|
|
121
|
+
than created at once.
|
|
122
|
+
- **Refused commands are reported.** A producer can refuse a command on the device's `reject`
|
|
123
|
+
topic, and Home Assistant fires `ildevice_command_rejected` with the reason. A control whose
|
|
124
|
+
`requires` condition does not hold is unavailable, rather than sending a command that will fail.
|
|
125
|
+
- **Availability comes from the producer too.** When a producer goes offline, all of its devices
|
|
126
|
+
go unavailable. The offline grace keeps a short outage from showing.
|
|
127
|
+
- **Unique ids can be kept.** Aliases map entity keys to the ones that an earlier integration
|
|
128
|
+
used (see [Keeping the ids](#keeping-the-ids-of-an-earlier-integration)).
|
|
129
|
+
- **It also runs without a broker.** Another integration can embed the consumer with an in-process
|
|
130
|
+
transport (see [Embedding](#embedding)).
|
|
131
|
+
|
|
132
|
+
## Layout
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
ildevice_ha/core/ Home Assistant free: descriptor, plan (entities), topics, values,
|
|
136
|
+
the consumer model, transport interface, in-process broker, icons
|
|
137
|
+
ildevice_ha/ the Home Assistant layer: hub (the model's sink), entity classes,
|
|
138
|
+
HA mqtt transport, attach; published to PyPI as `ildevice-homeassistant`
|
|
139
|
+
custom_components/ildevice/ the integration: config flow and platforms, requiring `ildevice-homeassistant`
|
|
140
|
+
tests/ core and model tests, the ildevice vectors, and the integration
|
|
141
|
+
in a test Home Assistant
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`core/plan.py` is where a descriptor becomes entities; nothing in it looks at a model.
|
|
145
|
+
`core/model.py` follows descriptors, values, presence and rejections on a `Transport` and tells a
|
|
146
|
+
`Sink` what changed, so the same core runs without Home Assistant.
|
|
147
|
+
|
|
148
|
+
### Embedding
|
|
149
|
+
|
|
150
|
+
Another integration that produces ildevice descriptors in the same process can use the consumer
|
|
151
|
+
instead of going through a broker. It lists `ildevice-homeassistant` in its manifest
|
|
152
|
+
`requirements` (the same pinned version as this integration's manifest), not this integration in
|
|
153
|
+
`dependencies`, and calls `ildevice_ha.attach.build_hub(hass, options, transport, platform)` with
|
|
154
|
+
its own transport (for example `core.memory.InProcessTransport`, an in-process broker with MQTT's
|
|
155
|
+
filters and retained messages) and its own domain. It forwards the platforms listed in
|
|
156
|
+
`const.PLATFORMS` to its config entry and hands each one's `async_add_entities` to
|
|
157
|
+
`hub.register_platform`.
|
|
158
|
+
|
|
159
|
+
`InProcessTransport` keeps retained values but does not keep a publish history by default.
|
|
160
|
+
Tests that inspect `transport.published` should construct it with `record_published=True`.
|
|
161
|
+
|
|
162
|
+
## Specification baseline
|
|
163
|
+
|
|
164
|
+
The MQTT consumer is tested against the conformance vectors of ildevice specification **v0.1.0** (draft `il: 0`),
|
|
165
|
+
commit [bc51a9e](https://github.com/3735943886/ildevice/tree/bc51a9ef8ac46770f67a02722c5c92907d1019ee).
|
|
166
|
+
CI pins that revision because draft changes can break compatibility without changing `il`.
|
|
167
|
+
Local vector tests use the checkout selected by `ILDEVICE` (or `../ildevice`), so they can
|
|
168
|
+
also check proposed specification changes before the baseline is advanced.
|
|
169
|
+
|
|
170
|
+
## Tests
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
python3.14 -m venv .venv
|
|
174
|
+
.venv/bin/python -m pip install -e ".[test]"
|
|
175
|
+
.venv/bin/python -m pytest
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
- `tests/fixtures/rusthinq/` holds the descriptors the rusthinq drivers publish (ten LG
|
|
179
|
+
appliances), which cover the composite kinds, `trigger` / `requires` and groups.
|
|
180
|
+
- `tests/fixtures/sample/` holds descriptors from a Tuya producer with its own topic layout: a
|
|
181
|
+
plug, bulb, curtain, lock, valve, siren and button.
|
|
182
|
+
- `tests/test_vectors.py` runs the language-neutral conformance vectors of the ildevice
|
|
183
|
+
repository. It looks for a checkout at `../ildevice`, or at `$ILDEVICE`, and is skipped without
|
|
184
|
+
one.
|
|
185
|
+
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""The IL consumer for Home Assistant as a library: `core` (no Home Assistant) and the Home Assistant layer on it (hub,
|
|
2
|
+
entity classes, the mqtt transport, `attach.build_hub`).
|
|
3
|
+
|
|
4
|
+
The `ildevice` integration (custom_components/ildevice) is a thin host of this package, and another integration can
|
|
5
|
+
embed it the same way. Importing this package or `core` needs no Home Assistant.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.2.0"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Build the IL consumer for a host: the Home Assistant integration itself, or another integration that embeds it.
|
|
2
|
+
|
|
3
|
+
An embedding integration passes its own `transport` (for example `core.memory.InProcessTransport` shared with an
|
|
4
|
+
in-process producer, so no broker is needed) and its own domain as `platform`, forwards the IL platforms to its
|
|
5
|
+
config entry, and hands each platform's `async_add_entities` to `hub.register_platform`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from collections.abc import Mapping
|
|
12
|
+
|
|
13
|
+
from homeassistant.core import HomeAssistant
|
|
14
|
+
|
|
15
|
+
from .const import (
|
|
16
|
+
CONF_ALIASES,
|
|
17
|
+
CONF_AUTO_ADD,
|
|
18
|
+
CONF_DEVICES,
|
|
19
|
+
CONF_IL_PREFIX,
|
|
20
|
+
CONF_OFFLINE_GRACE,
|
|
21
|
+
DOMAIN,
|
|
22
|
+
with_defaults,
|
|
23
|
+
)
|
|
24
|
+
from .core import parse_aliases
|
|
25
|
+
from .core.transport import Transport
|
|
26
|
+
from .hub import IlHub
|
|
27
|
+
|
|
28
|
+
_LOGGER = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def build_hub(
|
|
32
|
+
hass: HomeAssistant, options: Mapping, transport: Transport | None = None, platform: str = DOMAIN,
|
|
33
|
+
entry_id: str | None = None,
|
|
34
|
+
) -> IlHub:
|
|
35
|
+
"""`options` are the integration's options (`const.CONF_*`); `transport` defaults to Home Assistant's `mqtt`."""
|
|
36
|
+
options = with_defaults(options)
|
|
37
|
+
aliases = parse_aliases(options[CONF_ALIASES])
|
|
38
|
+
if aliases is None:
|
|
39
|
+
_LOGGER.error("the legacy entity id option is not valid JSON aliases; ignoring it")
|
|
40
|
+
return IlHub(
|
|
41
|
+
hass,
|
|
42
|
+
il_prefix=options[CONF_IL_PREFIX],
|
|
43
|
+
offline_grace=options[CONF_OFFLINE_GRACE],
|
|
44
|
+
aliases=aliases or {},
|
|
45
|
+
auto_add=options[CONF_AUTO_ADD],
|
|
46
|
+
allowed=set(options[CONF_DEVICES]),
|
|
47
|
+
transport=transport,
|
|
48
|
+
platform=platform,
|
|
49
|
+
entry_id=entry_id,
|
|
50
|
+
)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Constants of the IL integration."""
|
|
2
|
+
|
|
3
|
+
from .core.topics import DEFAULT_IL_PREFIX
|
|
4
|
+
|
|
5
|
+
DOMAIN = "ildevice"
|
|
6
|
+
|
|
7
|
+
CONF_IL_PREFIX = "il_prefix"
|
|
8
|
+
CONF_OFFLINE_GRACE = "offline_grace"
|
|
9
|
+
CONF_ALIASES = "aliases"
|
|
10
|
+
CONF_AUTO_ADD = "auto_add"
|
|
11
|
+
CONF_DEVICES = "devices" # ids of the devices the user chose to add
|
|
12
|
+
|
|
13
|
+
DEFAULT_OFFLINE_GRACE = 0 # seconds a device may report unavailable before entities say so
|
|
14
|
+
MAX_OFFLINE_GRACE = 3600
|
|
15
|
+
|
|
16
|
+
DEFAULT_OPTIONS = {
|
|
17
|
+
CONF_IL_PREFIX: DEFAULT_IL_PREFIX,
|
|
18
|
+
CONF_OFFLINE_GRACE: DEFAULT_OFFLINE_GRACE,
|
|
19
|
+
CONF_ALIASES: "",
|
|
20
|
+
CONF_AUTO_ADD: False,
|
|
21
|
+
CONF_DEVICES: (),
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
EVENT_COMMAND_REJECTED = f"{DOMAIN}_command_rejected"
|
|
25
|
+
|
|
26
|
+
PLATFORMS = [
|
|
27
|
+
"alarm_control_panel",
|
|
28
|
+
"binary_sensor",
|
|
29
|
+
"button",
|
|
30
|
+
"climate",
|
|
31
|
+
"cover",
|
|
32
|
+
"event",
|
|
33
|
+
"fan",
|
|
34
|
+
"humidifier",
|
|
35
|
+
"light",
|
|
36
|
+
"lock",
|
|
37
|
+
"number",
|
|
38
|
+
"select",
|
|
39
|
+
"sensor",
|
|
40
|
+
"siren",
|
|
41
|
+
"switch",
|
|
42
|
+
"text",
|
|
43
|
+
"vacuum",
|
|
44
|
+
"valve",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def with_defaults(options) -> dict:
|
|
49
|
+
"""An entry's options, each one it lacks at its default."""
|
|
50
|
+
return {**DEFAULT_OPTIONS, **options}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Home Assistant free core of the IL consumer: parse descriptors, locate and decode values,
|
|
2
|
+
encode commands, and plan which entities a descriptor becomes.
|
|
3
|
+
|
|
4
|
+
Nothing here imports Home Assistant, so it can be tested without it and reused by another
|
|
5
|
+
consumer. `plan.py` says *what* entities a device becomes; the integration's platform modules
|
|
6
|
+
turn each `EntitySpec` into a real entity.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .descriptor import Descriptor, DescriptorError, Prop, Requires, parse_descriptor
|
|
10
|
+
from .model import Device, IlModel, Sink, parse_aliases
|
|
11
|
+
from .plan import EntitySpec, plan_entities
|
|
12
|
+
from .topics import Topics, resolve_topics
|
|
13
|
+
from .transport import CallLater, Message, Transport
|
|
14
|
+
from .values import decode_value, encode_command
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"CallLater",
|
|
18
|
+
"Descriptor",
|
|
19
|
+
"DescriptorError",
|
|
20
|
+
"Device",
|
|
21
|
+
"EntitySpec",
|
|
22
|
+
"IlModel",
|
|
23
|
+
"Message",
|
|
24
|
+
"Prop",
|
|
25
|
+
"Requires",
|
|
26
|
+
"Sink",
|
|
27
|
+
"Topics",
|
|
28
|
+
"Transport",
|
|
29
|
+
"decode_value",
|
|
30
|
+
"encode_command",
|
|
31
|
+
"parse_aliases",
|
|
32
|
+
"parse_descriptor",
|
|
33
|
+
"plan_entities",
|
|
34
|
+
"resolve_topics",
|
|
35
|
+
]
|