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.
@@ -0,0 +1,4 @@
1
+ .venv/
2
+ __pycache__/
3
+ .pytest_cache/
4
+ *.pyc
@@ -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
+ ![IL device](custom_components/ildevice/brand/icon.png)
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
+ ![IL device](custom_components/ildevice/brand/icon.png)
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
+ ]