python-daikin-itm 0.1.0__tar.gz → 1.0.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.
- {python_daikin_itm-0.1.0 → python_daikin_itm-1.0.0}/PKG-INFO +9 -9
- {python_daikin_itm-0.1.0 → python_daikin_itm-1.0.0}/README.md +7 -7
- {python_daikin_itm-0.1.0 → python_daikin_itm-1.0.0}/daikin_itm/__init__.py +2 -0
- {python_daikin_itm-0.1.0 → python_daikin_itm-1.0.0}/daikin_itm/cli.py +15 -4
- {python_daikin_itm-0.1.0 → python_daikin_itm-1.0.0}/daikin_itm/client.py +5 -3
- {python_daikin_itm-0.1.0 → python_daikin_itm-1.0.0}/daikin_itm/exceptions.py +24 -0
- {python_daikin_itm-0.1.0 → python_daikin_itm-1.0.0}/daikin_itm/models.py +1 -1
- {python_daikin_itm-0.1.0 → python_daikin_itm-1.0.0}/pyproject.toml +1 -4
- {python_daikin_itm-0.1.0 → python_daikin_itm-1.0.0}/pyproject.toml.orig +2 -5
- {python_daikin_itm-0.1.0 → python_daikin_itm-1.0.0}/LICENSE +0 -0
- {python_daikin_itm-0.1.0 → python_daikin_itm-1.0.0}/daikin_itm/protocol.py +0 -0
- {python_daikin_itm-0.1.0 → python_daikin_itm-1.0.0}/daikin_itm/py.typed +0 -0
- {python_daikin_itm-0.1.0 → python_daikin_itm-1.0.0}/daikin_itm/state.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-daikin-itm
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: Asynchronous Python client for the Daikin intelligent Touch Manager (iTM).
|
|
5
5
|
Keywords: async,client,daikin,itm,intelligent touch manager,hvac
|
|
6
|
-
Author: Robbin Janssen
|
|
6
|
+
Author: Robbin Janssen
|
|
7
7
|
Author-email: Robbin Janssen <robbinjanssen@gmail.com>
|
|
8
8
|
License-Expression: MIT
|
|
9
9
|
License-File: LICENSE
|
|
@@ -52,8 +52,6 @@ A Python package to control the indoor units connected to a Daikin
|
|
|
52
52
|
built-in web UI, so it needs no cloud service and no extra hardware: only
|
|
53
53
|
network access to the iTM and a user account on it.
|
|
54
54
|
|
|
55
|
-
Based on the original work by Robin Mulder (Exonet).
|
|
56
|
-
|
|
57
55
|
Used by the Home Assistant integration
|
|
58
56
|
[home-assistant-daikin-itm](https://github.com/robbinjanssen/home-assistant-daikin-itm).
|
|
59
57
|
|
|
@@ -135,7 +133,7 @@ async def main() -> None:
|
|
|
135
133
|
"192.168.1.10",
|
|
136
134
|
"<your-username>",
|
|
137
135
|
"<your-password>",
|
|
138
|
-
zone_path=("
|
|
136
|
+
zone_path=("Building", "Indoor units"),
|
|
139
137
|
) as client:
|
|
140
138
|
units = await client.fetch_units()
|
|
141
139
|
for unit in units.values():
|
|
@@ -165,7 +163,7 @@ The other constructor arguments are keyword-only:
|
|
|
165
163
|
| :------- | :------ | :---------- |
|
|
166
164
|
| `session` | `None` | An `aiohttp.ClientSession` to use; the client creates (and closes) its own otherwise. |
|
|
167
165
|
| `store` | `MemorySessionStore()` | Where to keep the page token, see [Sessions](#sessions). |
|
|
168
|
-
| `zone_path` | `(
|
|
166
|
+
| `zone_path` | `()` | The path of zone names in the iTM's icon view to the zone that contains the indoor units, e.g. `("Building", "Indoor units")`. The default (`DEFAULT_ZONE_PATH`) is the top level; use `list_zones()` to find the path. |
|
|
169
167
|
| `commit_delay` | `3.0` | Seconds between a setpoint change and its commit, like the web UI. |
|
|
170
168
|
| `min_write_interval` | `5.0` | Minimum seconds between two writes to the same unit. |
|
|
171
169
|
| `confirm_timeout` | `30.0` | Seconds to wait for a fresh read to show a change. |
|
|
@@ -179,7 +177,7 @@ The other constructor arguments are keyword-only:
|
|
|
179
177
|
| `fetch_units()` | Read every unit in the zone path, returns `dict[str, Unit]` by name. |
|
|
180
178
|
| `fetch_unit(name)` | Read a single unit (cheaper than `fetch_units()`). |
|
|
181
179
|
| `fetch_capabilities()` | Read every unit's identity and the modes and fan control it offers, returns `dict[str, Capabilities]`. This costs four requests per unit, so do it once rather than on every poll. |
|
|
182
|
-
| `list_zones(path=())` | The names of the zones and units at a path, for discovery: `list_zones()` for the top level, `list_zones(("
|
|
180
|
+
| `list_zones(path=())` | The names of the zones and units at a path, for discovery: `list_zones()` for the top level, `list_zones(("Building",))` for what's inside `Building`. |
|
|
183
181
|
| `set_setpoint(unit, value, *, confirm=True)` | Change a unit's setpoint. Refused while the unit is stopped (the iTM ignores it then) or when the value is outside `setpoint_range`. |
|
|
184
182
|
| `apply(unit, *, power=None, mode=None, fan_speed=None, confirm=True)` | Change power, mode and/or fan speed in one write. The request is compared with the unit as read in the write session: only what differs is sent, and a unit that already is in the requested state is returned without writing anything. |
|
|
185
183
|
| `set_power(unit, on, *, confirm=True)` | Start or stop a unit. |
|
|
@@ -220,6 +218,7 @@ class SessionStore(Protocol):
|
|
|
220
218
|
| `SessionEnded` | The iTM ended the session (it answered with a LOGOUT update), e.g. because the token was stale. |
|
|
221
219
|
| `VersionMismatch` | The iTM expects another web UI version string (subclass of `SessionEnded`); carries it in `.version`. |
|
|
222
220
|
| `ItmCommandError` | The iTM answered a command with a dialog, the UI was not as expected, or a change was not confirmed in time. |
|
|
221
|
+
| `SetpointOutOfRange` | The unit does not accept the setpoint (subclass of `ItmCommandError`). The allowed setpoints depend on the unit and its operation mode; the ones it accepts now are in `.allowed`. |
|
|
223
222
|
| `ZoneNotFound` | A zone of the zone path is not there (subclass of `ItmCommandError`); the names that are there are in `.available`. |
|
|
224
223
|
|
|
225
224
|
Network errors are raised as `aiohttp.ClientError` or `TimeoutError`.
|
|
@@ -282,8 +281,9 @@ or `ITM_HOST`.
|
|
|
282
281
|
export ITM_HOST=192.168.1.10
|
|
283
282
|
export ITM_USERNAME=... ITM_PASSWORD=... # or: --creds creds.json
|
|
284
283
|
itm zones # top-level zones
|
|
285
|
-
itm zones
|
|
286
|
-
itm
|
|
284
|
+
itm zones Building # zones/units inside "Building"
|
|
285
|
+
itm --zone "Building/Indoor units" status
|
|
286
|
+
# all units in that zone (default: the top level); descriptions are
|
|
287
287
|
# cached in ~/.cache/daikin-itm/units.json (--refresh to re-read them)
|
|
288
288
|
itm caps # modes / fan control per unit
|
|
289
289
|
itm setpoint 1:1-04 21 # each write returns once the unit shows the change
|
|
@@ -23,8 +23,6 @@ A Python package to control the indoor units connected to a Daikin
|
|
|
23
23
|
built-in web UI, so it needs no cloud service and no extra hardware: only
|
|
24
24
|
network access to the iTM and a user account on it.
|
|
25
25
|
|
|
26
|
-
Based on the original work by Robin Mulder (Exonet).
|
|
27
|
-
|
|
28
26
|
Used by the Home Assistant integration
|
|
29
27
|
[home-assistant-daikin-itm](https://github.com/robbinjanssen/home-assistant-daikin-itm).
|
|
30
28
|
|
|
@@ -106,7 +104,7 @@ async def main() -> None:
|
|
|
106
104
|
"192.168.1.10",
|
|
107
105
|
"<your-username>",
|
|
108
106
|
"<your-password>",
|
|
109
|
-
zone_path=("
|
|
107
|
+
zone_path=("Building", "Indoor units"),
|
|
110
108
|
) as client:
|
|
111
109
|
units = await client.fetch_units()
|
|
112
110
|
for unit in units.values():
|
|
@@ -136,7 +134,7 @@ The other constructor arguments are keyword-only:
|
|
|
136
134
|
| :------- | :------ | :---------- |
|
|
137
135
|
| `session` | `None` | An `aiohttp.ClientSession` to use; the client creates (and closes) its own otherwise. |
|
|
138
136
|
| `store` | `MemorySessionStore()` | Where to keep the page token, see [Sessions](#sessions). |
|
|
139
|
-
| `zone_path` | `(
|
|
137
|
+
| `zone_path` | `()` | The path of zone names in the iTM's icon view to the zone that contains the indoor units, e.g. `("Building", "Indoor units")`. The default (`DEFAULT_ZONE_PATH`) is the top level; use `list_zones()` to find the path. |
|
|
140
138
|
| `commit_delay` | `3.0` | Seconds between a setpoint change and its commit, like the web UI. |
|
|
141
139
|
| `min_write_interval` | `5.0` | Minimum seconds between two writes to the same unit. |
|
|
142
140
|
| `confirm_timeout` | `30.0` | Seconds to wait for a fresh read to show a change. |
|
|
@@ -150,7 +148,7 @@ The other constructor arguments are keyword-only:
|
|
|
150
148
|
| `fetch_units()` | Read every unit in the zone path, returns `dict[str, Unit]` by name. |
|
|
151
149
|
| `fetch_unit(name)` | Read a single unit (cheaper than `fetch_units()`). |
|
|
152
150
|
| `fetch_capabilities()` | Read every unit's identity and the modes and fan control it offers, returns `dict[str, Capabilities]`. This costs four requests per unit, so do it once rather than on every poll. |
|
|
153
|
-
| `list_zones(path=())` | The names of the zones and units at a path, for discovery: `list_zones()` for the top level, `list_zones(("
|
|
151
|
+
| `list_zones(path=())` | The names of the zones and units at a path, for discovery: `list_zones()` for the top level, `list_zones(("Building",))` for what's inside `Building`. |
|
|
154
152
|
| `set_setpoint(unit, value, *, confirm=True)` | Change a unit's setpoint. Refused while the unit is stopped (the iTM ignores it then) or when the value is outside `setpoint_range`. |
|
|
155
153
|
| `apply(unit, *, power=None, mode=None, fan_speed=None, confirm=True)` | Change power, mode and/or fan speed in one write. The request is compared with the unit as read in the write session: only what differs is sent, and a unit that already is in the requested state is returned without writing anything. |
|
|
156
154
|
| `set_power(unit, on, *, confirm=True)` | Start or stop a unit. |
|
|
@@ -191,6 +189,7 @@ class SessionStore(Protocol):
|
|
|
191
189
|
| `SessionEnded` | The iTM ended the session (it answered with a LOGOUT update), e.g. because the token was stale. |
|
|
192
190
|
| `VersionMismatch` | The iTM expects another web UI version string (subclass of `SessionEnded`); carries it in `.version`. |
|
|
193
191
|
| `ItmCommandError` | The iTM answered a command with a dialog, the UI was not as expected, or a change was not confirmed in time. |
|
|
192
|
+
| `SetpointOutOfRange` | The unit does not accept the setpoint (subclass of `ItmCommandError`). The allowed setpoints depend on the unit and its operation mode; the ones it accepts now are in `.allowed`. |
|
|
194
193
|
| `ZoneNotFound` | A zone of the zone path is not there (subclass of `ItmCommandError`); the names that are there are in `.available`. |
|
|
195
194
|
|
|
196
195
|
Network errors are raised as `aiohttp.ClientError` or `TimeoutError`.
|
|
@@ -253,8 +252,9 @@ or `ITM_HOST`.
|
|
|
253
252
|
export ITM_HOST=192.168.1.10
|
|
254
253
|
export ITM_USERNAME=... ITM_PASSWORD=... # or: --creds creds.json
|
|
255
254
|
itm zones # top-level zones
|
|
256
|
-
itm zones
|
|
257
|
-
itm
|
|
255
|
+
itm zones Building # zones/units inside "Building"
|
|
256
|
+
itm --zone "Building/Indoor units" status
|
|
257
|
+
# all units in that zone (default: the top level); descriptions are
|
|
258
258
|
# cached in ~/.cache/daikin-itm/units.json (--refresh to re-read them)
|
|
259
259
|
itm caps # modes / fan control per unit
|
|
260
260
|
itm setpoint 1:1-04 21 # each write returns once the unit shows the change
|
|
@@ -13,6 +13,7 @@ from .exceptions import (
|
|
|
13
13
|
LoginRejected,
|
|
14
14
|
ProtocolError,
|
|
15
15
|
SessionEnded,
|
|
16
|
+
SetpointOutOfRange,
|
|
16
17
|
VersionMismatch,
|
|
17
18
|
ZoneNotFound,
|
|
18
19
|
)
|
|
@@ -31,6 +32,7 @@ __all__ = [
|
|
|
31
32
|
"ProtocolError",
|
|
32
33
|
"SessionEnded",
|
|
33
34
|
"SessionStore",
|
|
35
|
+
"SetpointOutOfRange",
|
|
34
36
|
"Unit",
|
|
35
37
|
"VersionMismatch",
|
|
36
38
|
"ZoneNotFound",
|
|
@@ -234,11 +234,19 @@ async def _run(args: argparse.Namespace) -> None:
|
|
|
234
234
|
def _parser() -> argparse.ArgumentParser:
|
|
235
235
|
"""Build the argument parser."""
|
|
236
236
|
p = argparse.ArgumentParser(prog="itm", description=(__doc__ or "").splitlines()[0])
|
|
237
|
-
p.add_argument(
|
|
237
|
+
p.add_argument(
|
|
238
|
+
"--host",
|
|
239
|
+
default=os.environ.get("ITM_HOST"),
|
|
240
|
+
help="address of the iTM (default: $ITM_HOST)",
|
|
241
|
+
)
|
|
238
242
|
p.add_argument(
|
|
239
243
|
"--creds", help="JSON file with username/password fields (1Password export)"
|
|
240
244
|
)
|
|
241
|
-
p.add_argument(
|
|
245
|
+
p.add_argument(
|
|
246
|
+
"--zone",
|
|
247
|
+
help="zone path to the units, e.g. 'Building/Indoor units' "
|
|
248
|
+
"(default: the top level)",
|
|
249
|
+
)
|
|
242
250
|
p.add_argument(
|
|
243
251
|
"--session-file",
|
|
244
252
|
help=f"where to keep the session token (default: per host in {CACHE_DIR})",
|
|
@@ -251,7 +259,7 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
251
259
|
help="re-read descriptions instead of using the cache",
|
|
252
260
|
)
|
|
253
261
|
z = sub.add_parser("zones", help="list zones/units at a path")
|
|
254
|
-
z.add_argument("path", nargs="?", help="e.g. '
|
|
262
|
+
z.add_argument("path", nargs="?", help="e.g. 'Building'")
|
|
255
263
|
s = sub.add_parser("setpoint", help="change a unit's setpoint")
|
|
256
264
|
s.add_argument("unit")
|
|
257
265
|
s.add_argument("value", type=int)
|
|
@@ -279,7 +287,10 @@ def main(argv: Sequence[str] | None = None) -> None:
|
|
|
279
287
|
argv: The arguments, defaults to the ones the process was started with.
|
|
280
288
|
|
|
281
289
|
"""
|
|
282
|
-
|
|
290
|
+
parser = _parser()
|
|
291
|
+
args = parser.parse_args(argv)
|
|
292
|
+
if not args.host:
|
|
293
|
+
parser.error("set the iTM's address with --host or ITM_HOST")
|
|
283
294
|
|
|
284
295
|
async def runner() -> None:
|
|
285
296
|
task = asyncio.current_task()
|
|
@@ -29,6 +29,7 @@ from .exceptions import (
|
|
|
29
29
|
LoginRejected,
|
|
30
30
|
ProtocolError,
|
|
31
31
|
SessionEnded,
|
|
32
|
+
SetpointOutOfRange,
|
|
32
33
|
VersionMismatch,
|
|
33
34
|
ZoneNotFound,
|
|
34
35
|
)
|
|
@@ -95,7 +96,9 @@ ICON_BTN_TOP, ICON_BTN_DOWN, ICON_BTN_UP, ICON_PANEL = 1, 2, 3, 6
|
|
|
95
96
|
ICON_ID_ZONE = 1000
|
|
96
97
|
LOGIN_BUTTON = 4
|
|
97
98
|
|
|
98
|
-
|
|
99
|
+
# The top level of the icon view. Most iTMs group the units in zones; pass the path
|
|
100
|
+
# of zone names to them, see list_zones().
|
|
101
|
+
DEFAULT_ZONE_PATH: tuple[str, ...] = ()
|
|
99
102
|
|
|
100
103
|
_T = TypeVar("_T")
|
|
101
104
|
|
|
@@ -807,8 +810,7 @@ class ItmClient:
|
|
|
807
810
|
raise ItmCommandError(msg)
|
|
808
811
|
nums = list(current.setpoint_range)
|
|
809
812
|
if value not in nums:
|
|
810
|
-
|
|
811
|
-
raise ItmCommandError(msg)
|
|
813
|
+
raise SetpointOutOfRange(unit, value, current.setpoint_range)
|
|
812
814
|
if current.setpoint == value:
|
|
813
815
|
return current
|
|
814
816
|
await s.choose(P_SETPOINT, nums.index(value))
|
|
@@ -59,3 +59,27 @@ class ZoneNotFound(ItmCommandError): # noqa: N818 - public API name
|
|
|
59
59
|
super().__init__(f"{zone!r} not found here; available: {available}")
|
|
60
60
|
self.zone = zone
|
|
61
61
|
self.available = available
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class SetpointOutOfRange(ItmCommandError): # noqa: N818 - public API name
|
|
65
|
+
"""The unit does not accept the setpoint; carries the setpoints it does accept.
|
|
66
|
+
|
|
67
|
+
The allowed setpoints depend on the unit and its operation mode, so they may have
|
|
68
|
+
changed since the unit was last read.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(self, unit: str, value: int, allowed: tuple[int, ...]) -> None:
|
|
72
|
+
"""Initialize the error.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
----
|
|
76
|
+
unit: The name of the unit.
|
|
77
|
+
value: The setpoint that was asked for.
|
|
78
|
+
allowed: The setpoints the unit accepts now, as read while writing.
|
|
79
|
+
|
|
80
|
+
"""
|
|
81
|
+
msg = f"{unit}: setpoint {value} outside allowed range {list(allowed)}"
|
|
82
|
+
super().__init__(msg)
|
|
83
|
+
self.unit = unit
|
|
84
|
+
self.value = value
|
|
85
|
+
self.allowed = allowed
|
|
@@ -85,7 +85,7 @@ class Capabilities:
|
|
|
85
85
|
point_id: str | None = None # iTM management point ID, e.g. "105"
|
|
86
86
|
# Bus port:address, e.g. "1:1-04"; stable, survives renames.
|
|
87
87
|
address: str | None = None
|
|
88
|
-
unit_type: str | None = None # e.g. "
|
|
88
|
+
unit_type: str | None = None # e.g. "Indoor unit", in the iTM's language
|
|
89
89
|
description: str | None = None # free text set in the iTM
|
|
90
90
|
zones: tuple[str, ...] = field(default_factory=tuple) # zone paths of the unit
|
|
91
91
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "python-daikin-itm"
|
|
3
|
-
version = "
|
|
3
|
+
version = "1.0.0"
|
|
4
4
|
description = "Asynchronous Python client for the Daikin intelligent Touch Manager (iTM)."
|
|
5
5
|
license = "MIT"
|
|
6
6
|
license-files = ["LICENSE"]
|
|
@@ -31,9 +31,6 @@ dependencies = ["aiohttp>=3.9.0"]
|
|
|
31
31
|
name = "Robbin Janssen"
|
|
32
32
|
email = "robbinjanssen@gmail.com"
|
|
33
33
|
|
|
34
|
-
[[project.authors]]
|
|
35
|
-
name = "Robin Mulder"
|
|
36
|
-
|
|
37
34
|
[[project.maintainers]]
|
|
38
35
|
name = "Robbin Janssen"
|
|
39
36
|
email = "robbinjanssen@gmail.com"
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "python-daikin-itm"
|
|
3
3
|
# The version is set from the release tag by the release workflow.
|
|
4
|
-
version = "
|
|
4
|
+
version = "1.0.0"
|
|
5
5
|
description = "Asynchronous Python client for the Daikin intelligent Touch Manager (iTM)."
|
|
6
|
-
authors = [
|
|
7
|
-
{ name = "Robbin Janssen", email = "robbinjanssen@gmail.com" },
|
|
8
|
-
{ name = "Robin Mulder" },
|
|
9
|
-
]
|
|
6
|
+
authors = [{ name = "Robbin Janssen", email = "robbinjanssen@gmail.com" }]
|
|
10
7
|
maintainers = [{ name = "Robbin Janssen", email = "robbinjanssen@gmail.com" }]
|
|
11
8
|
license = "MIT"
|
|
12
9
|
license-files = ["LICENSE"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|