python-tado 0.18.2__tar.gz → 0.18.4__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.
Files changed (32) hide show
  1. {python_tado-0.18.2/python_tado.egg-info → python_tado-0.18.4}/PKG-INFO +2 -2
  2. {python_tado-0.18.2 → python_tado-0.18.4}/PyTado/const.py +2 -0
  3. {python_tado-0.18.2 → python_tado-0.18.4}/PyTado/http.py +3 -1
  4. {python_tado-0.18.2 → python_tado-0.18.4}/PyTado/interface/api/hops_tado.py +77 -23
  5. {python_tado-0.18.2 → python_tado-0.18.4}/PyTado/interface/api/my_tado.py +32 -35
  6. {python_tado-0.18.2 → python_tado-0.18.4}/PyTado/zone/hops_zone.py +17 -10
  7. {python_tado-0.18.2 → python_tado-0.18.4}/PyTado/zone/my_zone.py +17 -14
  8. {python_tado-0.18.2 → python_tado-0.18.4}/pyproject.toml +2 -1
  9. {python_tado-0.18.2 → python_tado-0.18.4/python_tado.egg-info}/PKG-INFO +2 -2
  10. {python_tado-0.18.2 → python_tado-0.18.4}/python_tado.egg-info/SOURCES.txt +1 -0
  11. python_tado-0.18.4/tests/test_hops_zone.py +147 -0
  12. {python_tado-0.18.2 → python_tado-0.18.4}/tests/test_http.py +12 -7
  13. {python_tado-0.18.2 → python_tado-0.18.4}/tests/test_my_zone.py +32 -0
  14. {python_tado-0.18.2 → python_tado-0.18.4}/AUTHORS +0 -0
  15. {python_tado-0.18.2 → python_tado-0.18.4}/LICENSE +0 -0
  16. {python_tado-0.18.2 → python_tado-0.18.4}/PyTado/__init__.py +0 -0
  17. {python_tado-0.18.2 → python_tado-0.18.4}/PyTado/__main__.py +0 -0
  18. {python_tado-0.18.2 → python_tado-0.18.4}/PyTado/exceptions.py +0 -0
  19. {python_tado-0.18.2 → python_tado-0.18.4}/PyTado/interface/__init__.py +0 -0
  20. {python_tado-0.18.2 → python_tado-0.18.4}/PyTado/interface/api/__init__.py +0 -0
  21. {python_tado-0.18.2 → python_tado-0.18.4}/PyTado/interface/interface.py +0 -0
  22. {python_tado-0.18.2 → python_tado-0.18.4}/PyTado/logger.py +0 -0
  23. {python_tado-0.18.2 → python_tado-0.18.4}/PyTado/zone/__init__.py +0 -0
  24. {python_tado-0.18.2 → python_tado-0.18.4}/README.md +0 -0
  25. {python_tado-0.18.2 → python_tado-0.18.4}/python_tado.egg-info/dependency_links.txt +0 -0
  26. {python_tado-0.18.2 → python_tado-0.18.4}/python_tado.egg-info/entry_points.txt +0 -0
  27. {python_tado-0.18.2 → python_tado-0.18.4}/python_tado.egg-info/not-zip-safe +0 -0
  28. {python_tado-0.18.2 → python_tado-0.18.4}/python_tado.egg-info/requires.txt +0 -0
  29. {python_tado-0.18.2 → python_tado-0.18.4}/python_tado.egg-info/top_level.txt +0 -0
  30. {python_tado-0.18.2 → python_tado-0.18.4}/setup.cfg +0 -0
  31. {python_tado-0.18.2 → python_tado-0.18.4}/tests/test_my_tado.py +0 -0
  32. {python_tado-0.18.2 → python_tado-0.18.4}/tests/test_tado_interface.py +0 -0
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-tado
3
- Version: 0.18.2
3
+ Version: 0.18.4
4
4
  Summary: PyTado from chrism0dwk, modfied by w.malgadey, diplix, michaelarnauts, LenhartStephan, splifter, syssi, andersonshatch, Yippy, p0thi, Coffee2CodeNL, chiefdragon, FilBr, nikilase, albertomontesg, Moritz-Schmidt, palazzem
5
- Author-email: Chris Jewell <chrism0dwk@gmail.com>, "w.malgadey" <w.malgadey@gmail.com>
5
+ Author-email: Chris Jewell <chrism0dwk@gmail.com>, "w.malgadey" <w.malgadey@gmail.com>, FilBr <filippo.barba@protonmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
7
7
  Version 3, 29 June 2007
8
8
 
@@ -21,6 +21,7 @@ CONST_MODE_DRY = "DRY"
21
21
  CONST_MODE_FAN = "FAN"
22
22
 
23
23
  CONST_LINK_OFFLINE = "OFFLINE"
24
+ CONST_CONNECTION_OFFLINE = "OFFLINE"
24
25
 
25
26
  CONST_FAN_OFF = "OFF"
26
27
  CONST_FAN_AUTO = "AUTO"
@@ -99,6 +100,7 @@ TADO_MODES_WITH_NO_TEMP_SETTING = [
99
100
  ]
100
101
 
101
102
  DEFAULT_TADO_PRECISION = 0.1
103
+ DEFAULT_TADOX_PRECISION = 0.01
102
104
 
103
105
  HOME_DOMAIN = "homes"
104
106
  DEVICE_DOMAIN = "devices"
@@ -25,6 +25,8 @@ class Endpoint(enum.StrEnum):
25
25
  HOPS_API = "https://hops.tado.com/"
26
26
  MOBILE = "https://my.tado.com/mobile/1.9/"
27
27
  EIQ = "https://energy-insights.tado.com/api/"
28
+ TARIFF = "https://tariff-experience.tado.com/api/"
29
+ GENIE = "https://genie.tado.com/api/v2/"
28
30
 
29
31
 
30
32
  class Domain(enum.StrEnum):
@@ -282,7 +284,7 @@ class Http:
282
284
  f"Unknown error while refreshing token with status code {response.status_code}"
283
285
  )
284
286
 
285
- def _login(self) -> tuple[int, bool, str] | None:
287
+ def _login(self) -> tuple[int, str] | None:
286
288
 
287
289
  headers = self._headers
288
290
  headers["Content-Type"] = "application/json"
@@ -12,6 +12,7 @@ from ...logger import Logger
12
12
  from ...exceptions import TadoNotSupportedException
13
13
  from ...http import (
14
14
  Action,
15
+ Domain,
15
16
  Http,
16
17
  Mode,
17
18
  TadoRequest,
@@ -28,7 +29,7 @@ class TadoX(Tado):
28
29
 
29
30
  Example usage: http = Http('me@somewhere.com', 'mypasswd')
30
31
  t = TadoX(http)
31
- t.get_climate(1) # Get climate, zone 1.
32
+ t.get_climate(1) # Get climate, room 1.
32
33
  """
33
34
 
34
35
  def __init__(
@@ -56,28 +57,36 @@ class TadoX(Tado):
56
57
  # set to None until explicitly set
57
58
  self._auto_geofencing_supported = None
58
59
 
59
- def _create_request(self) -> TadoRequest:
60
- return TadoXRequest()
61
-
62
60
  def get_devices(self):
63
61
  """
64
62
  Gets device information.
65
63
  """
66
64
 
67
- request = self._create_request()
65
+ request = TadoXRequest()
68
66
  request.command = "roomsAndDevices"
69
67
 
70
- rooms: list[dict[str, Any]] = self._http.request(request)["rooms"]
68
+ rooms_and_devices: list[dict[str, Any]] = self._http.request(request)
69
+ rooms = rooms_and_devices["rooms"]
70
+
71
71
  devices = [device for room in rooms for device in room["devices"]]
72
72
 
73
+ for device in devices:
74
+ request = TadoXRequest()
75
+ request.domain = Domain.DEVICES
76
+ request.device = device["serialNo"]
77
+ device.update(self._http.request(request))
78
+
79
+ if "otherDevices" in rooms_and_devices:
80
+ devices.append(rooms_and_devices["otherDevices"])
81
+
73
82
  return devices
74
83
 
75
84
  def get_zones(self):
76
85
  """
77
- Gets zones information.
86
+ Gets zones (or rooms in Tado X API) information.
78
87
  """
79
88
 
80
- request = self._create_request()
89
+ request = TadoXRequest()
81
90
  request.command = "roomsAndDevices"
82
91
 
83
92
  return self._http.request(request)["rooms"]
@@ -94,7 +103,7 @@ class TadoX(Tado):
94
103
  Gets current states of all zones.
95
104
  """
96
105
 
97
- request = self._create_request()
106
+ request = TadoXRequest()
98
107
  request.command = "rooms"
99
108
 
100
109
  return self._http.request(request)
@@ -104,7 +113,7 @@ class TadoX(Tado):
104
113
  Gets current state of Zone.
105
114
  """
106
115
 
107
- request = self._create_request()
116
+ request = TadoXRequest()
108
117
  request.command = f"rooms/{zone:d}"
109
118
  data = self._http.request(request)
110
119
 
@@ -139,7 +148,7 @@ class TadoX(Tado):
139
148
  """
140
149
 
141
150
  raise TadoNotSupportedException(
142
- "This method is not currently supported by the Tado X API"
151
+ "Tado X API only support seven days timetable"
143
152
  )
144
153
 
145
154
  def get_schedule(
@@ -150,17 +159,54 @@ class TadoX(Tado):
150
159
  Zone has 3 different schedules, one for each timetable (see setTimetable)
151
160
  """
152
161
 
153
- request = self._create_request()
162
+ request = TadoXRequest()
154
163
  request.command = f"rooms/{zone:d}/schedule"
155
164
 
156
165
  return self._http.request(request)
157
166
 
158
167
  def set_schedule(self, zone, timetable: Timetable, day, data):
159
168
  """
160
- Set the schedule for a zone, day is required
169
+ Set the schedule for a zone, day is not required for Tado X API.
170
+
171
+ example data
172
+ [
173
+ {
174
+ "start": "00:00",
175
+ "end": "07:05",
176
+ "dayType": "MONDAY",
177
+ "setting": {
178
+ "power": "ON",
179
+ "temperature": {
180
+ "value": 18
181
+ }
182
+ }
183
+ },
184
+ {
185
+ "start": "07:05",
186
+ "end": "22:05",
187
+ "dayType": "MONDAY",
188
+ "setting": {
189
+ "power": "ON",
190
+ "temperature": {
191
+ "value": 22
192
+ }
193
+ }
194
+ },
195
+ {
196
+ "start": "22:05",
197
+ "end": "24:00",
198
+ "dayType": "MONDAY",
199
+ "setting": {
200
+ "power": "ON",
201
+ "temperature": {
202
+ "value": 18
203
+ }
204
+ }
205
+ }
206
+ ]
161
207
  """
162
208
 
163
- request = self._create_request()
209
+ request = TadoXRequest()
164
210
  request.command = f"rooms/{zone:d}/schedule"
165
211
  request.action = Action.SET
166
212
  request.payload = data
@@ -173,7 +219,7 @@ class TadoX(Tado):
173
219
  Delete current overlay
174
220
  """
175
221
 
176
- request = self._create_request()
222
+ request = TadoXRequest()
177
223
  request.command = f"rooms/{zone:d}/resumeSchedule"
178
224
  request.action = Action.SET
179
225
 
@@ -195,7 +241,7 @@ class TadoX(Tado):
195
241
  horizontal_swing=None,
196
242
  ):
197
243
  """
198
- Set current overlay for a zone
244
+ Set current overlay for a zone, a room in Tado X API.
199
245
  """
200
246
 
201
247
  post_data = {
@@ -213,7 +259,7 @@ class TadoX(Tado):
213
259
  if duration is not None:
214
260
  post_data["termination"]["durationInSeconds"] = duration
215
261
 
216
- request = self._create_request()
262
+ request = TadoXRequest()
217
263
  request.command = f"rooms/{zone:d}/manualControl"
218
264
  request.action = Action.SET
219
265
  request.payload = post_data
@@ -226,7 +272,7 @@ class TadoX(Tado):
226
272
  """
227
273
 
228
274
  raise TadoNotSupportedException(
229
- "This method is not currently supported by the Tado X API"
275
+ "Concept of zones is not available by Tado X API, they use rooms"
230
276
  )
231
277
 
232
278
  def get_open_window_detected(self, zone):
@@ -236,7 +282,7 @@ class TadoX(Tado):
236
282
 
237
283
  data = self.get_state(zone)
238
284
 
239
- if "activated" in data["openWindow"]:
285
+ if data["openWindow"] and "activated" in data["openWindow"]:
240
286
  return {"openWindowDetected": True}
241
287
  else:
242
288
  return {"openWindowDetected": False}
@@ -266,16 +312,24 @@ class TadoX(Tado):
266
312
  with option to get specific info i.e. cmd='temperatureOffset'
267
313
  """
268
314
 
269
- raise TadoNotSupportedException(
270
- "This method is not currently supported by the Tado X API"
271
- )
315
+ if cmd:
316
+ request = TadoRequest()
317
+ request.command = cmd
318
+ else:
319
+ request = TadoXRequest()
320
+
321
+ request.action = Action.GET
322
+ request.domain = Domain.DEVICES
323
+ request.device = device_id
324
+
325
+ return self._http.request(request)
272
326
 
273
327
  def set_temp_offset(self, device_id, offset=0, measure="celsius"):
274
328
  """
275
329
  Set the Temperature offset on the device.
276
330
  """
277
331
 
278
- request = self._create_request()
332
+ request = TadoXRequest()
279
333
  request.command = f"roomsAndDevices/devices/{device_id}"
280
334
  request.action = Action.CHANGE
281
335
  request.payload = {"temperatureOffset": offset}
@@ -65,15 +65,12 @@ class Tado:
65
65
  # set to None until explicitly set
66
66
  self._auto_geofencing_supported = None
67
67
 
68
- def _create_request(self) -> TadoRequest:
69
- return TadoRequest()
70
-
71
68
  def get_me(self):
72
69
  """
73
70
  Gets home information.
74
71
  """
75
72
 
76
- request = self._create_request()
73
+ request = TadoRequest()
77
74
  request.action = Action.GET
78
75
  request.domain = Domain.ME
79
76
 
@@ -84,7 +81,7 @@ class Tado:
84
81
  Gets device information.
85
82
  """
86
83
 
87
- request = self._create_request()
84
+ request = TadoRequest()
88
85
  request.command = "devices"
89
86
  return self._http.request(request)
90
87
 
@@ -93,7 +90,7 @@ class Tado:
93
90
  Gets zones information.
94
91
  """
95
92
 
96
- request = self._create_request()
93
+ request = TadoRequest()
97
94
  request.command = "zones"
98
95
 
99
96
  return self._http.request(request)
@@ -110,7 +107,7 @@ class Tado:
110
107
  Gets current states of all zones.
111
108
  """
112
109
 
113
- request = self._create_request()
110
+ request = TadoRequest()
114
111
  request.command = "zoneStates"
115
112
 
116
113
  return self._http.request(request)
@@ -120,7 +117,7 @@ class Tado:
120
117
  Gets current state of Zone.
121
118
  """
122
119
 
123
- request = self._create_request()
120
+ request = TadoRequest()
124
121
  request.command = f"zones/{zone}/state"
125
122
  data = {
126
123
  **self._http.request(request),
@@ -150,7 +147,7 @@ class Tado:
150
147
  # indicates whether presence is current locked (manually set) to
151
148
  # HOME or AWAY or not locked (automatically set based on geolocation)
152
149
 
153
- request = self._create_request()
150
+ request = TadoRequest()
154
151
  request.command = "state"
155
152
  data = self._http.request(request)
156
153
 
@@ -186,7 +183,7 @@ class Tado:
186
183
  Gets current capabilities of zone.
187
184
  """
188
185
 
189
- request = self._create_request()
186
+ request = TadoRequest()
190
187
  request.command = f"zones/{zone:d}/capabilities"
191
188
 
192
189
  return self._http.request(request)
@@ -207,7 +204,7 @@ class Tado:
207
204
  Get the Timetable type currently active
208
205
  """
209
206
 
210
- request = self._create_request()
207
+ request = TadoRequest()
211
208
  request.command = f"zones/{zone:d}/schedule/activeTimetable"
212
209
  request.mode = Mode.PLAIN
213
210
  data = self._http.request(request)
@@ -231,7 +228,7 @@ class Tado:
231
228
  "Incorrect date format, should be YYYY-MM-DD"
232
229
  ) from err
233
230
 
234
- request = self._create_request()
231
+ request = TadoRequest()
235
232
  request.command = (
236
233
  f"zones/{zone:d}/dayReport?date={day.strftime('%Y-%m-%d')}"
237
234
  )
@@ -245,7 +242,7 @@ class Tado:
245
242
  id = 3 : SEVEN_DAY (MONDAY, TUESDAY, WEDNESDAY ...)
246
243
  """
247
244
 
248
- request = self._create_request()
245
+ request = TadoRequest()
249
246
  request.command = f"zones/{zone:d}/schedule/activeTimetable"
250
247
  request.action = Action.CHANGE
251
248
  request.payload = {"id": timetable}
@@ -260,7 +257,7 @@ class Tado:
260
257
  Get the JSON representation of the schedule for a zone.
261
258
  Zone has 3 different schedules, one for each timetable (see setTimetable)
262
259
  """
263
- request = self._create_request()
260
+ request = TadoRequest()
264
261
  if day:
265
262
  request.command = (
266
263
  f"zones/{zone:d}/schedule/timetables/{timetable:d}/blocks/{day}"
@@ -278,7 +275,7 @@ class Tado:
278
275
  Set the schedule for a zone, day is required
279
276
  """
280
277
 
281
- request = self._create_request()
278
+ request = TadoRequest()
282
279
  request.command = (
283
280
  f"zones/{zone:d}/schedule/timetables/{timetable:d}/blocks/{day}"
284
281
  )
@@ -293,7 +290,7 @@ class Tado:
293
290
  Gets outside weather data
294
291
  """
295
292
 
296
- request = self._create_request()
293
+ request = TadoRequest()
297
294
  request.command = "weather"
298
295
 
299
296
  return self._http.request(request)
@@ -303,7 +300,7 @@ class Tado:
303
300
  Gets air quality information
304
301
  """
305
302
 
306
- request = self._create_request()
303
+ request = TadoRequest()
307
304
  request.command = "airComfort"
308
305
 
309
306
  return self._http.request(request)
@@ -313,7 +310,7 @@ class Tado:
313
310
  Gets active users in home
314
311
  """
315
312
 
316
- request = self._create_request()
313
+ request = TadoRequest()
317
314
  request.command = "users"
318
315
 
319
316
  return self._http.request(request)
@@ -323,7 +320,7 @@ class Tado:
323
320
  Gets information about mobile devices
324
321
  """
325
322
 
326
- request = self._create_request()
323
+ request = TadoRequest()
327
324
  request.command = "mobileDevices"
328
325
 
329
326
  return self._http.request(request)
@@ -333,7 +330,7 @@ class Tado:
333
330
  Delete current overlay
334
331
  """
335
332
 
336
- request = self._create_request()
333
+ request = TadoRequest()
337
334
  request.command = f"zones/{zone:d}/overlay"
338
335
  request.action = Action.RESET
339
336
  request.mode = Mode.PLAIN
@@ -384,7 +381,7 @@ class Tado:
384
381
  if duration is not None:
385
382
  post_data["termination"]["durationInSeconds"] = duration
386
383
 
387
- request = self._create_request()
384
+ request = TadoRequest()
388
385
  request.command = f"zones/{zone:d}/overlay"
389
386
  request.action = Action.CHANGE
390
387
  request.payload = post_data
@@ -396,7 +393,7 @@ class Tado:
396
393
  Get current overlay default settings for zone.
397
394
  """
398
395
 
399
- request = self._create_request()
396
+ request = TadoRequest()
400
397
  request.command = f"zones/{zone:d}/defaultOverlay"
401
398
 
402
399
  return self._http.request(request)
@@ -420,7 +417,7 @@ class Tado:
420
417
  Sets HomeState to presence
421
418
  """
422
419
 
423
- request = self._create_request()
420
+ request = TadoRequest()
424
421
  request.command = "presenceLock"
425
422
  request.action = Action.CHANGE
426
423
  request.payload = {"homePresence": presence}
@@ -434,7 +431,7 @@ class Tado:
434
431
 
435
432
  # Only attempt to set Auto Geofencing if it is believed to be supported
436
433
  if self._auto_geofencing_supported:
437
- request = self._create_request()
434
+ request = TadoRequest()
438
435
  request.command = "presenceLock"
439
436
  request.action = Action.RESET
440
437
 
@@ -469,7 +466,7 @@ class Tado:
469
466
  Note: This can only be set if an open window was detected in this zone
470
467
  """
471
468
 
472
- request = self._create_request()
469
+ request = TadoRequest()
473
470
  request.command = f"zones/{zone:d}/state/openWindow/activate"
474
471
  request.action = Action.SET
475
472
  request.mode = Mode.PLAIN
@@ -481,7 +478,7 @@ class Tado:
481
478
  Sets the window in zone to closed
482
479
  """
483
480
 
484
- request = self._create_request()
481
+ request = TadoRequest()
485
482
  request.command = f"zones/{zone:d}/state/openWindow"
486
483
  request.action = Action.RESET
487
484
  request.mode = Mode.PLAIN
@@ -494,7 +491,7 @@ class Tado:
494
491
  with option to get specific info i.e. cmd='temperatureOffset'
495
492
  """
496
493
 
497
- request = self._create_request()
494
+ request = TadoRequest()
498
495
  request.command = cmd
499
496
  request.action = Action.GET
500
497
  request.domain = Domain.DEVICES
@@ -507,7 +504,7 @@ class Tado:
507
504
  Set the Temperature offset on the device.
508
505
  """
509
506
 
510
- request = self._create_request()
507
+ request = TadoRequest()
511
508
  request.command = "temperatureOffset"
512
509
  request.action = Action.CHANGE
513
510
  request.domain = Domain.DEVICES
@@ -521,7 +518,7 @@ class Tado:
521
518
  Get Energy IQ tariff history
522
519
  """
523
520
 
524
- request = self._create_request()
521
+ request = TadoRequest()
525
522
  request.command = "tariffs"
526
523
  request.action = Action.GET
527
524
  request.endpoint = Endpoint.EIQ
@@ -533,7 +530,7 @@ class Tado:
533
530
  Get Energy IQ meter readings
534
531
  """
535
532
 
536
- request = self._create_request()
533
+ request = TadoRequest()
537
534
  request.command = "meterReadings"
538
535
  request.action = Action.GET
539
536
  request.endpoint = Endpoint.EIQ
@@ -547,7 +544,7 @@ class Tado:
547
544
  Send Meter Readings to Tado, date format is YYYY-MM-DD, reading is without decimals
548
545
  """
549
546
 
550
- request = self._create_request()
547
+ request = TadoRequest()
551
548
  request.command = "meterReadings"
552
549
  request.action = Action.SET
553
550
  request.endpoint = Endpoint.EIQ
@@ -585,7 +582,7 @@ class Tado:
585
582
  "startDate": from_date,
586
583
  }
587
584
 
588
- request = self._create_request()
585
+ request = TadoRequest()
589
586
  request.command = "tariffs"
590
587
  request.action = Action.SET
591
588
  request.endpoint = Endpoint.EIQ
@@ -598,7 +595,7 @@ class Tado:
598
595
  Gets available heating circuits
599
596
  """
600
597
 
601
- request = self._create_request()
598
+ request = TadoRequest()
602
599
  request.command = "heatingCircuits"
603
600
 
604
601
  return self._http.request(request)
@@ -608,7 +605,7 @@ class Tado:
608
605
  Get zone control information
609
606
  """
610
607
 
611
- request = self._create_request()
608
+ request = TadoRequest()
612
609
  request.command = f"zones/{zone:d}/control"
613
610
 
614
611
  return self._http.request(request)
@@ -618,7 +615,7 @@ class Tado:
618
615
  Sets the heating circuit for a zone
619
616
  """
620
617
 
621
- request = self._create_request()
618
+ request = TadoRequest()
622
619
  request.command = f"zones/{zone:d}/control/heatingCircuit"
623
620
  request.action = Action.CHANGE
624
621
  request.payload = {"circuitNumber": heating_circuit}
@@ -7,14 +7,16 @@ import logging
7
7
  from typing import Any, Self
8
8
 
9
9
  from PyTado.const import (
10
- CONST_HORIZONTAL_SWING_OFF,
11
10
  CONST_HVAC_HEAT,
12
11
  CONST_HVAC_IDLE,
13
12
  CONST_HVAC_OFF,
14
- CONST_LINK_OFFLINE,
13
+ CONST_CONNECTION_OFFLINE,
15
14
  CONST_MODE_HEAT,
16
15
  CONST_MODE_OFF,
16
+ CONST_MODE_SMART_SCHEDULE,
17
17
  CONST_VERTICAL_SWING_OFF,
18
+ CONST_HORIZONTAL_SWING_OFF,
19
+ DEFAULT_TADOX_PRECISION,
18
20
  )
19
21
  from .my_zone import TadoZone
20
22
 
@@ -26,6 +28,8 @@ _LOGGER = logging.getLogger(__name__)
26
28
  class TadoXZone(TadoZone):
27
29
  """Tado Zone data structure for hops.tado.com (Tado X) API."""
28
30
 
31
+ precision: float = DEFAULT_TADOX_PRECISION
32
+
29
33
  @classmethod
30
34
  def from_data(cls, zone_id: int, data: dict[str, Any]) -> Self:
31
35
  """Handle update callbacks for X zones with specific parsing."""
@@ -37,9 +41,9 @@ class TadoXZone(TadoZone):
37
41
  sensor_data = data["sensorDataPoints"]
38
42
 
39
43
  if "insideTemperature" in sensor_data:
40
- kwargs["current_temp"] = float(
41
- sensor_data["insideTemperature"]["value"]
42
- )
44
+ inside_temp = sensor_data["insideTemperature"]
45
+ if "value" in inside_temp:
46
+ kwargs["current_temp"] = float(inside_temp["value"])
43
47
  kwargs["current_temp_timestamp"] = None
44
48
  if "precision" in sensor_data["insideTemperature"]:
45
49
  kwargs["precision"] = sensor_data["insideTemperature"][
@@ -123,20 +127,23 @@ class TadoXZone(TadoZone):
123
127
  manual_termination["projectedExpiry"]
124
128
  )
125
129
  else:
130
+ kwargs["current_hvac_mode"] = CONST_MODE_SMART_SCHEDULE
126
131
  kwargs["overlay_termination_type"] = None
127
132
  kwargs["overlay_termination_timestamp"] = None
133
+ else:
134
+ kwargs["current_hvac_mode"] = CONST_MODE_SMART_SCHEDULE
128
135
 
129
- # Connection state and availability
130
- kwargs["connection"] = data.get("connectionState", {}).get("value")
131
- kwargs["available"] = kwargs.get("link") != CONST_LINK_OFFLINE
136
+ kwargs["available"] = (
137
+ kwargs.get("connection") != CONST_CONNECTION_OFFLINE
138
+ )
132
139
 
133
140
  # Termination conditions
134
141
  if "terminationCondition" in data:
135
142
  kwargs["default_overlay_termination_type"] = data[
136
143
  "terminationCondition"
137
- ].get("type")
144
+ ].get("type", None)
138
145
  kwargs["default_overlay_termination_duration"] = data[
139
146
  "terminationCondition"
140
- ].get("durationInSeconds")
147
+ ].get("durationInSeconds", None)
141
148
 
142
149
  return cls(zone_id=zone_id, **kwargs)
@@ -60,13 +60,13 @@ class TadoZone:
60
60
  tado_mode: str | None = None
61
61
  overlay_termination_type: str | None = None
62
62
  overlay_termination_timestamp: str | None = None
63
+ default_overlay_termination_type: str | None = None
64
+ default_overlay_termination_duration: str | None = None
63
65
  preparation: bool = False
64
66
  open_window: bool = False
65
67
  open_window_detected: bool = False
66
68
  open_window_attr: dict[str, Any] = dataclasses.field(default_factory=dict)
67
69
  precision: float = DEFAULT_TADO_PRECISION
68
- default_overlay_termination_type = None
69
- default_overlay_termination_duration = None
70
70
 
71
71
  @property
72
72
  def overlay_active(self) -> bool:
@@ -82,8 +82,9 @@ class TadoZone:
82
82
  sensor_data = data["sensorDataPoints"]
83
83
 
84
84
  if "insideTemperature" in sensor_data:
85
- temperature = float(sensor_data["insideTemperature"]["celsius"])
86
- kwargs["current_temp"] = temperature
85
+ kwargs["current_temp"] = float(
86
+ sensor_data["insideTemperature"]["celsius"]
87
+ )
87
88
  kwargs["current_temp_timestamp"] = sensor_data[
88
89
  "insideTemperature"
89
90
  ]["timestamp"]
@@ -115,21 +116,23 @@ class TadoZone:
115
116
  "temperature" in data["setting"]
116
117
  and data["setting"]["temperature"] is not None
117
118
  ):
118
- target_temperature = float(
119
+ kwargs["target_temp"] = float(
119
120
  data["setting"]["temperature"]["celsius"]
120
121
  )
121
- kwargs["target_temp"] = target_temperature
122
122
 
123
123
  setting = data["setting"]
124
124
 
125
- kwargs["current_fan_speed"] = None
126
- kwargs["current_fan_level"] = None
127
- # If there is no overlay, the mode will always be
128
- # "SMART_SCHEDULE"
129
- kwargs["current_hvac_mode"] = CONST_MODE_OFF
130
- kwargs["current_swing_mode"] = CONST_MODE_OFF
131
- kwargs["current_vertical_swing_mode"] = CONST_VERTICAL_SWING_OFF
132
- kwargs["current_horizontal_swing_mode"] = CONST_HORIZONTAL_SWING_OFF
125
+ # Reset modes and settings
126
+ kwargs.update(
127
+ {
128
+ "current_fan_speed": None,
129
+ "current_fan_level": None,
130
+ "current_hvac_mode": CONST_MODE_OFF,
131
+ "current_swing_mode": CONST_MODE_OFF,
132
+ "current_vertical_swing_mode": CONST_VERTICAL_SWING_OFF,
133
+ "current_horizontal_swing_mode": CONST_HORIZONTAL_SWING_OFF,
134
+ }
135
+ )
133
136
 
134
137
  if "mode" in setting:
135
138
  # v3 devices use mode
@@ -4,11 +4,12 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-tado"
7
- version = "0.18.2"
7
+ version = "0.18.4"
8
8
  description = "PyTado from chrism0dwk, modfied by w.malgadey, diplix, michaelarnauts, LenhartStephan, splifter, syssi, andersonshatch, Yippy, p0thi, Coffee2CodeNL, chiefdragon, FilBr, nikilase, albertomontesg, Moritz-Schmidt, palazzem"
9
9
  authors = [
10
10
  { name = "Chris Jewell", email = "chrism0dwk@gmail.com" },
11
11
  { name = "w.malgadey", email = "w.malgadey@gmail.com" },
12
+ { name = "FilBr", email = "filippo.barba@protonmail.com" },
12
13
  ]
13
14
  dependencies = ["requests"]
14
15
  requires-python = ">=3.11"
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-tado
3
- Version: 0.18.2
3
+ Version: 0.18.4
4
4
  Summary: PyTado from chrism0dwk, modfied by w.malgadey, diplix, michaelarnauts, LenhartStephan, splifter, syssi, andersonshatch, Yippy, p0thi, Coffee2CodeNL, chiefdragon, FilBr, nikilase, albertomontesg, Moritz-Schmidt, palazzem
5
- Author-email: Chris Jewell <chrism0dwk@gmail.com>, "w.malgadey" <w.malgadey@gmail.com>
5
+ Author-email: Chris Jewell <chrism0dwk@gmail.com>, "w.malgadey" <w.malgadey@gmail.com>, FilBr <filippo.barba@protonmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
7
7
  Version 3, 29 June 2007
8
8
 
@@ -23,6 +23,7 @@ python_tado.egg-info/entry_points.txt
23
23
  python_tado.egg-info/not-zip-safe
24
24
  python_tado.egg-info/requires.txt
25
25
  python_tado.egg-info/top_level.txt
26
+ tests/test_hops_zone.py
26
27
  tests/test_http.py
27
28
  tests/test_my_tado.py
28
29
  tests/test_my_zone.py
@@ -0,0 +1,147 @@
1
+ """Test the TadoZone object."""
2
+
3
+ import json
4
+ import unittest
5
+ from unittest import mock
6
+
7
+ from . import common
8
+
9
+ from PyTado.http import Http
10
+ from PyTado.interface.api import TadoX
11
+
12
+
13
+ class TadoZoneTestCase(unittest.TestCase):
14
+ """Test cases for zone class"""
15
+
16
+ def setUp(self) -> None:
17
+ super().setUp()
18
+ login_patch = mock.patch(
19
+ "PyTado.http.Http._login", return_value=(1, "foo")
20
+ )
21
+ is_x_line_patch = mock.patch(
22
+ "PyTado.http.Http._check_x_line_generation", return_value=True
23
+ )
24
+ get_me_patch = mock.patch("PyTado.interface.api.Tado.get_me")
25
+ login_patch.start()
26
+ is_x_line_patch.start()
27
+ get_me_patch.start()
28
+ self.addCleanup(login_patch.stop)
29
+ self.addCleanup(is_x_line_patch.stop)
30
+ self.addCleanup(get_me_patch.stop)
31
+
32
+ self.http = Http("my@username.com", "mypassword")
33
+ self.tado_client = TadoX(self.http)
34
+
35
+ def set_fixture(self, filename: str) -> None:
36
+ def check_get_state(zone_id):
37
+ assert zone_id == 1
38
+ return json.loads(common.load_fixture(filename))
39
+
40
+ get_state_patch = mock.patch(
41
+ "PyTado.interface.api.TadoX.get_state",
42
+ side_effect=check_get_state,
43
+ )
44
+ get_state_patch.start()
45
+ self.addCleanup(get_state_patch.stop)
46
+
47
+ def test_tadox_heating_auto_mode(self):
48
+ """Test general homes response."""
49
+
50
+ self.set_fixture("home_1234/tadox.heating.auto_mode.json")
51
+ mode = self.tado_client.get_zone_state(1)
52
+
53
+ assert mode.ac_power is None
54
+ assert mode.ac_power_timestamp is None
55
+ assert mode.available is True
56
+ assert mode.connection == "CONNECTED"
57
+ assert mode.current_fan_speed is None
58
+ assert mode.current_humidity == 38.00
59
+ assert mode.current_humidity_timestamp is None
60
+ assert mode.current_hvac_action == "HEATING"
61
+ assert mode.current_hvac_mode == "SMART_SCHEDULE"
62
+ assert mode.current_swing_mode == "OFF"
63
+ assert mode.current_temp == 24.00
64
+ assert mode.current_temp_timestamp is None
65
+ assert mode.heating_power is None
66
+ assert mode.heating_power_percentage == 100.0
67
+ assert mode.heating_power_timestamp is None
68
+ assert mode.is_away is None
69
+ assert mode.link is None
70
+ assert mode.open_window is False
71
+ assert not mode.open_window_attr
72
+ assert mode.overlay_active is False
73
+ assert mode.overlay_termination_type is None
74
+ assert mode.power == "ON"
75
+ assert mode.precision == 0.01
76
+ assert mode.preparation is False
77
+ assert mode.tado_mode is None
78
+ assert mode.target_temp == 22.0
79
+ assert mode.zone_id == 1
80
+
81
+ def test_tadox_heating_manual_mode(self):
82
+ """Test general homes response."""
83
+
84
+ self.set_fixture("home_1234/tadox.heating.manual_mode.json")
85
+ mode = self.tado_client.get_zone_state(1)
86
+
87
+ assert mode.ac_power is None
88
+ assert mode.ac_power_timestamp is None
89
+ assert mode.available is True
90
+ assert mode.connection == "CONNECTED"
91
+ assert mode.current_fan_speed is None
92
+ assert mode.current_humidity == 38.00
93
+ assert mode.current_humidity_timestamp is None
94
+ assert mode.current_hvac_action == "HEATING"
95
+ assert mode.current_hvac_mode == "HEAT"
96
+ assert mode.current_swing_mode == "OFF"
97
+ assert mode.current_temp == 24.07
98
+ assert mode.current_temp_timestamp is None
99
+ assert mode.heating_power is None
100
+ assert mode.heating_power_percentage == 100.0
101
+ assert mode.heating_power_timestamp is None
102
+ assert mode.is_away is None
103
+ assert mode.link is None
104
+ assert mode.open_window is False
105
+ assert not mode.open_window_attr
106
+ assert mode.overlay_active is True
107
+ assert mode.overlay_termination_type == "NEXT_TIME_BLOCK"
108
+ assert mode.power == "ON"
109
+ assert mode.precision == 0.01
110
+ assert mode.preparation is False
111
+ assert mode.tado_mode is None
112
+ assert mode.target_temp == 25.5
113
+ assert mode.zone_id == 1
114
+
115
+ def test_tadox_heating_manual_off(self):
116
+ """Test general homes response."""
117
+
118
+ self.set_fixture("home_1234/tadox.heating.manual_off.json")
119
+ mode = self.tado_client.get_zone_state(1)
120
+
121
+ assert mode.ac_power is None
122
+ assert mode.ac_power_timestamp is None
123
+ assert mode.available is True
124
+ assert mode.connection == "CONNECTED"
125
+ assert mode.current_fan_speed is None
126
+ assert mode.current_humidity == 38.00
127
+ assert mode.current_humidity_timestamp is None
128
+ assert mode.current_hvac_action == "OFF"
129
+ assert mode.current_hvac_mode == "OFF"
130
+ assert mode.current_swing_mode == "OFF"
131
+ assert mode.current_temp == 24.08
132
+ assert mode.current_temp_timestamp is None
133
+ assert mode.heating_power is None
134
+ assert mode.heating_power_percentage == 0.0
135
+ assert mode.heating_power_timestamp is None
136
+ assert mode.is_away is None
137
+ assert mode.link is None
138
+ assert mode.open_window is False
139
+ assert not mode.open_window_attr
140
+ assert mode.overlay_active is True
141
+ assert mode.overlay_termination_type == "NEXT_TIME_BLOCK"
142
+ assert mode.power == "OFF"
143
+ assert mode.precision == 0.01
144
+ assert mode.preparation is False
145
+ assert mode.tado_mode is None
146
+ assert mode.target_temp is None
147
+ assert mode.zone_id == 1
@@ -25,7 +25,7 @@ class TestHttp(unittest.TestCase):
25
25
  "https://auth.tado.com/oauth/token",
26
26
  json={
27
27
  "access_token": "value",
28
- "expires_in": 1234,
28
+ "expires_in": 1000,
29
29
  "refresh_token": "another_value",
30
30
  },
31
31
  status=200,
@@ -33,16 +33,19 @@ class TestHttp(unittest.TestCase):
33
33
 
34
34
  responses.add(
35
35
  responses.GET,
36
- "https://my.tado.com/api/v2/homes/1234/",
37
- json={"homes": [{"id": 1234}]},
36
+ "https://my.tado.com/api/v2/me",
37
+ json=json.loads(common.load_fixture("home_1234/my_api_v2_me.json")),
38
38
  status=200,
39
39
  )
40
40
 
41
- # Mock the get me response
42
41
  responses.add(
43
42
  responses.GET,
44
- "https://my.tado.com/api/v2/me",
45
- json={"homes": [{"id": 1234}]},
43
+ "https://my.tado.com/api/v2/homes/1234/",
44
+ json=json.loads(
45
+ common.load_fixture(
46
+ "home_1234/tadov2.my_api_v2_home_state.json"
47
+ )
48
+ ),
46
49
  status=200,
47
50
  )
48
51
 
@@ -102,7 +105,9 @@ class TestHttp(unittest.TestCase):
102
105
  responses.replace(
103
106
  responses.GET,
104
107
  "https://my.tado.com/api/v2/homes/1234/",
105
- json=json.loads(common.load_fixture("tadox/homes_response.json")),
108
+ json=json.loads(
109
+ common.load_fixture("home_1234/tadox.my_api_v2_home_state.json")
110
+ ),
106
111
  status=200,
107
112
  )
108
113
 
@@ -67,6 +67,38 @@ class TadoZoneTestCase(unittest.TestCase):
67
67
  assert mode.precision == 0.1
68
68
  assert mode.current_swing_mode == "OFF"
69
69
 
70
+ def test_my_api_issue_88(self):
71
+ """Test smart ac cool mode."""
72
+ self.set_fixture("my_api_issue_88.termination_condition.json")
73
+ mode = self.tado_client.get_zone_state(1)
74
+
75
+ assert mode.ac_power is None
76
+ assert mode.ac_power_timestamp is None
77
+ assert mode.available is True
78
+ assert mode.connection is None
79
+ assert mode.current_fan_speed is None
80
+ assert mode.current_humidity == 64.40
81
+ assert mode.current_humidity_timestamp == "2024-12-19T14:14:52.404Z"
82
+ assert mode.current_hvac_action == "HEATING"
83
+ assert mode.current_hvac_mode == "HEAT"
84
+ assert mode.current_swing_mode == "OFF"
85
+ assert mode.current_temp == 16.2
86
+ assert mode.current_temp_timestamp == "2024-12-19T14:14:52.404Z"
87
+ assert mode.heating_power is None
88
+ assert mode.heating_power_percentage == 100.0
89
+ assert mode.heating_power_timestamp == "2024-12-19T14:14:15.558Z"
90
+ assert mode.is_away is False
91
+ assert mode.link == "ONLINE"
92
+ assert mode.open_window is False
93
+ assert not mode.open_window_attr
94
+ assert mode.overlay_active
95
+ assert mode.overlay_termination_type == "TIMER"
96
+ assert mode.power == "ON"
97
+ assert mode.precision == 0.1
98
+ assert mode.preparation is False
99
+ assert mode.tado_mode == "HOME"
100
+ assert mode.target_temp == 22.0
101
+
70
102
  def test_smartac3_smart_mode(self):
71
103
  """Test smart ac smart mode."""
72
104
  self.set_fixture("smartac3.smart_mode.json")
File without changes
File without changes
File without changes
File without changes