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.
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-daikin-itm
3
- Version: 0.1.0
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, Robin Mulder
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=("Alles", "Binnendeel"),
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` | `("Alles", "Binnendeel")` | The zone path in the iTM's icon view that contains the indoor units (`DEFAULT_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(("Alles",))` for what's inside `Alles`. |
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 Alles # zones/units inside "Alles"
286
- itm status # all units in Alles/Binnendeel (--zone to change); descriptions are
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=("Alles", "Binnendeel"),
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` | `("Alles", "Binnendeel")` | The zone path in the iTM's icon view that contains the indoor units (`DEFAULT_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(("Alles",))` for what's inside `Alles`. |
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 Alles # zones/units inside "Alles"
257
- itm status # all units in Alles/Binnendeel (--zone to change); descriptions are
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("--host", default=os.environ.get("ITM_HOST", "10.50.3.46"))
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("--zone", help="zone path to the units, e.g. 'Alles/Binnendeel'")
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. 'Alles'")
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
- args = _parser().parse_args(argv)
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
- DEFAULT_ZONE_PATH = ("Alles", "Binnendeel")
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
- msg = f"{unit}: setpoint {value} outside allowed range {nums}"
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. "Binnendeel"
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 = "0.1.0"
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 = "0.1.0"
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"]