dirigera 1.2.7__tar.gz → 1.3.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.
Files changed (51) hide show
  1. {dirigera-1.2.7/src/dirigera.egg-info → dirigera-1.3.0}/PKG-INFO +111 -11
  2. {dirigera-1.2.7 → dirigera-1.3.0}/README.md +110 -10
  3. {dirigera-1.2.7 → dirigera-1.3.0}/pyproject.toml +1 -1
  4. {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/devices/air_purifier.py +2 -1
  5. {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/devices/base_ikea_model.py +4 -1
  6. {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/devices/blinds.py +17 -1
  7. {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/devices/controller.py +2 -1
  8. {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/devices/device.py +1 -4
  9. dirigera-1.3.0/src/dirigera/devices/electrical_sensor.py +39 -0
  10. {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/devices/environment_sensor.py +2 -1
  11. {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/devices/light.py +18 -7
  12. {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/devices/light_sensor.py +2 -1
  13. {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/devices/motion_sensor.py +2 -1
  14. {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/devices/occupancy_sensor.py +2 -1
  15. {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/devices/open_close_sensor.py +2 -1
  16. {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/devices/outlet.py +2 -1
  17. {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/devices/scene.py +44 -5
  18. {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/devices/water_sensor.py +2 -1
  19. {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/hub/hub.py +95 -3
  20. {dirigera-1.2.7 → dirigera-1.3.0/src/dirigera.egg-info}/PKG-INFO +111 -11
  21. {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera.egg-info/SOURCES.txt +4 -0
  22. {dirigera-1.2.7 → dirigera-1.3.0}/tests/test_blinds.py +18 -1
  23. dirigera-1.3.0/tests/test_electrical_sensor.py +94 -0
  24. dirigera-1.3.0/tests/test_hub.py +93 -0
  25. dirigera-1.3.0/tests/test_hub_get_by_name.py +156 -0
  26. {dirigera-1.2.7 → dirigera-1.3.0}/tests/test_light.py +68 -0
  27. {dirigera-1.2.7 → dirigera-1.3.0}/tests/test_outlet.py +19 -0
  28. {dirigera-1.2.7 → dirigera-1.3.0}/tests/test_scenes.py +130 -3
  29. {dirigera-1.2.7 → dirigera-1.3.0}/LICENSE +0 -0
  30. {dirigera-1.2.7 → dirigera-1.3.0}/setup.cfg +0 -0
  31. {dirigera-1.2.7 → dirigera-1.3.0}/setup.py +0 -0
  32. {dirigera-1.2.7 → dirigera-1.3.0}/src/__init__.py +0 -0
  33. {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/__init__.py +0 -0
  34. {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/devices/__init__.py +0 -0
  35. {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/hub/__init__.py +0 -0
  36. {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/hub/abstract_smart_home_hub.py +0 -0
  37. {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/hub/auth.py +0 -0
  38. {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/hub/utils.py +0 -0
  39. {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera.egg-info/dependency_links.txt +0 -0
  40. {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera.egg-info/entry_points.txt +0 -0
  41. {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera.egg-info/requires.txt +0 -0
  42. {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera.egg-info/top_level.txt +0 -0
  43. {dirigera-1.2.7 → dirigera-1.3.0}/tests/test_air_purifier.py +0 -0
  44. {dirigera-1.2.7 → dirigera-1.3.0}/tests/test_controller.py +0 -0
  45. {dirigera-1.2.7 → dirigera-1.3.0}/tests/test_environment_sensor.py +0 -0
  46. {dirigera-1.2.7 → dirigera-1.3.0}/tests/test_light_sensor.py +0 -0
  47. {dirigera-1.2.7 → dirigera-1.3.0}/tests/test_motion_sensor.py +0 -0
  48. {dirigera-1.2.7 → dirigera-1.3.0}/tests/test_occupancy_sensor.py +0 -0
  49. {dirigera-1.2.7 → dirigera-1.3.0}/tests/test_open_close_sensor.py +0 -0
  50. {dirigera-1.2.7 → dirigera-1.3.0}/tests/test_utils.py +0 -0
  51. {dirigera-1.2.7 → dirigera-1.3.0}/tests/test_water_sensor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dirigera
3
- Version: 1.2.7
3
+ Version: 1.3.0
4
4
  Summary: An unofficial Python client for controlling the IKEA Dirigera Smart Home Hub
5
5
  Author-email: Leggin <legginsun@gmail.com>
6
6
  License: MIT License
@@ -74,6 +74,7 @@ This repository provides an unofficial Python client for controlling the IKEA Di
74
74
  - [Creating a Scene](#creating-a-scene)
75
75
  - [Motion Sensor](#motion-sensor)
76
76
  - [Open Close Sensor](#open-close-sensor)
77
+ - [Electrical Sensor](#electrical-sensor)
77
78
  - [Event Listener](#event-listener)
78
79
  - [Motivation](#motivation)
79
80
  - [Contributing](#contributing)
@@ -144,6 +145,9 @@ response = dirigera_hub.delete(route="/scenes/1")
144
145
  # Fetch all air purifiers registered in the hub
145
146
  air_purifiers = dirigera_hub.get_air_purifiers()
146
147
 
148
+ # Fetch an air purifier by its name
149
+ air_purifier = dirigera_hub.get_air_purifier_by_name(air_purifier_name="living room air purifier")
150
+
147
151
  # Fetch an air purifier by its ID
148
152
  air_purifier = dirigera_hub.get_air_purifier_by_id(id_="1")
149
153
 
@@ -168,6 +172,9 @@ outlet = dirigera_hub.get_outlet_by_id(id_="1")
168
172
  # Fetch all environment sensors registered in the hub
169
173
  sensors = dirigera_hub.get_environment_sensors()
170
174
 
175
+ # Fetch an environment sensor by its name
176
+ sensor = dirigera_hub.get_environment_sensor_by_name(environment_sensor_name="living room environment sensor")
177
+
171
178
  # Fetch an environment sensor by its ID
172
179
  sensor = dirigera_hub.get_environment_sensor_by_id(id_="1")
173
180
 
@@ -183,6 +190,9 @@ sensor = dirigera_hub.get_motion_sensor_by_id(id_="1")
183
190
  # Fetch all open/close sensors registered in the hub
184
191
  sensors = dirigera_hub.get_open_close_sensors()
185
192
 
193
+ # Fetch an open/close sensor by its name
194
+ sensor = dirigera_hub.get_open_close_sensor_by_name(open_close_sensor_name="front door sensor")
195
+
186
196
  # Fetch an open/close sensor by its ID
187
197
  sensor = dirigera_hub.get_open_close_by_id(id_="1")
188
198
 
@@ -210,24 +220,42 @@ scenes = dirigera_hub.get_scenes()
210
220
  # Fetch a scene by its ID
211
221
  scene = dirigera_hub.get_scene_by_id(scene_id="1")
212
222
 
223
+ # Fetch a scene by its name
224
+ scene = dirigera_hub.get_scene_by_name(scene_name="movie night")
225
+
213
226
  # Fetch all water sensors registered in the hub
214
227
  water_sensors = dirigera_hub.get_water_sensors()
215
228
 
229
+ # Fetch a water sensor by its name
230
+ water_sensor = dirigera_hub.get_water_sensor_by_name(water_sensor_name="bathroom water sensor")
231
+
216
232
  # Fetch a water sensor by its ID
217
233
  water_sensor = dirigera_hub.get_water_sensor_by_id(id_="1")
218
234
 
219
235
  # Fetch all occupancy sensors registered in the hub
220
236
  occupancy_sensors = dirigera_hub.get_occupancy_sensors()
221
237
 
238
+ # Fetch an occupancy sensor by its name
239
+ occupancy_sensor = dirigera_hub.get_occupancy_sensor_by_name(occupancy_sensor_name="hallway occupancy sensor")
240
+
222
241
  # Fetch an occupancy sensor by its ID
223
242
  occupancy_sensor = dirigera_hub.get_occupancy_sensor_by_id(id_="1")
224
243
 
225
244
  # Fetch all light sensors registered in the hub
226
245
  light_sensors = dirigera_hub.get_light_sensors()
227
246
 
247
+ # Fetch a light sensor by its name
248
+ light_sensor = dirigera_hub.get_light_sensor_by_name(light_sensor_name="living room light sensor")
249
+
228
250
  # Fetch a light sensor by its ID
229
251
  light_sensor = dirigera_hub.get_light_sensor_by_id(id_="1")
230
252
 
253
+ # Fetch all electrical sensors registered in the hub
254
+ electrical_sensors = dirigera_hub.get_electrical_sensors()
255
+
256
+ # Fetch an electrical sensor by its ID
257
+ electrical_sensor = dirigera_hub.get_electrical_sensor_by_id(id_="1")
258
+
231
259
  # Fetch all devices registered in the hub
232
260
  devices = dirigera_hub.get_all_devices()
233
261
 
@@ -247,7 +275,8 @@ dirigera_hub.delete_scene(scene_id="1")
247
275
 
248
276
  # [Devices](./src/dirigera/devices/device.py)
249
277
 
250
- All available devices (Light, Controller, Outlet, ...) consist of the core data defined in [device.py](./src/dirigera/devices/device.py):
278
+ All available devices (Light, Controller, Outlet, ...) consist of the core data defined in [device.py](./src/dirigera/devices/device.py).
279
+ Calling `reload()` on a device (or scene) fetches the current data from the hub and updates the object in place. It also returns the same object, so `light = light.reload()` works as well.
251
280
 
252
281
  ### Core Device Data
253
282
 
@@ -324,7 +353,7 @@ light_level: Optional[int] = None
324
353
  color_temperature: Optional[int] = None
325
354
  color_temperature_min: Optional[int] = None
326
355
  color_temperature_max: Optional[int] = None
327
- color_hue: Optional[int] = None
356
+ color_hue: Optional[float] = None
328
357
  color_saturation: Optional[float] = None
329
358
  ```
330
359
 
@@ -381,7 +410,7 @@ time_of_last_energy_reset: Optional[datetime.datetime] = None
381
410
  Available methods for outlet are:
382
411
 
383
412
  ```python
384
- # Reload the air outlet data from the hub
413
+ # Reload the outlet data from the hub
385
414
  outlet.reload()
386
415
 
387
416
  # Set the name of the outlet
@@ -417,7 +446,7 @@ filter_lifetime: int
417
446
  current_p_m25: int
418
447
  ```
419
448
 
420
- Available methods for blinds are:
449
+ Available methods for air purifier are:
421
450
 
422
451
  ```python
423
452
  # Reload the air purifier data from the hub
@@ -467,6 +496,9 @@ blind.set_name(name="kitchen blind 1")
467
496
 
468
497
  # Set the target level of the blind (0-100)
469
498
  blind.set_target_level(target_level=90)
499
+
500
+ # Move the blind up or down, or stop it at its current position
501
+ blind.set_state(state=BlindsStateEnum.STOPPED)
470
502
  ```
471
503
 
472
504
  ## [Remote Controllers](./src/dirigera/devices/controller.py)
@@ -549,7 +581,7 @@ created_at: datetime.datetime
549
581
  last_completed: Optional[datetime.datetime] = None
550
582
  last_triggered: Optional[datetime.datetime] = None
551
583
  last_undo: Optional[datetime.datetime] = None
552
- commands: List[str]
584
+ commands: List[Union[str, Dict[str, Any]]]
553
585
  undo_allowed_duration: int
554
586
  ```
555
587
 
@@ -633,7 +665,7 @@ All available icons can be found here: [Icons](./src/dirigera/devices/scene.py)
633
665
  To get information about the available motion sensors, you can use the `get_motion_sensors()` method:
634
666
 
635
667
  ```python
636
- motions_sensors = dirigera_hub.get_motion_sensors()
668
+ motion_sensors = dirigera_hub.get_motion_sensors()
637
669
  ```
638
670
 
639
671
  The motion sensor object has the following attributes (additional to the core attributes):
@@ -645,7 +677,7 @@ light_level: Optional[float] = None
645
677
  is_detected: Optional[bool] = False
646
678
  ```
647
679
 
648
- Available methods for outlet are:
680
+ Available methods for motion sensor are:
649
681
 
650
682
  ```python
651
683
  # Reload the motion sensor data from the hub
@@ -663,6 +695,8 @@ To get information about the available open/close sensors, you can use the `get_
663
695
  open_close_sensors = dirigera_hub.get_open_close_sensors()
664
696
  ```
665
697
 
698
+ The hub does not guarantee the order of the returned devices, so look sensors up by their id (`get_open_close_by_id()`) or name (`get_open_close_sensor_by_name()`) instead of by their position in the list.
699
+
666
700
  The open_close_sensor object has the following attributes (additional to the core attributes):
667
701
 
668
702
  ```python
@@ -670,7 +704,7 @@ is_open: bool
670
704
  battery_percentage: Optional[int] = None
671
705
  ```
672
706
 
673
- Available methods for outlet are:
707
+ Available methods for open/close sensor are:
674
708
 
675
709
  ```python
676
710
  # Reload the open/close sensor data from the hub
@@ -680,6 +714,50 @@ open_close_sensor.reload()
680
714
  open_close_sensor.set_name(name="window 1")
681
715
  ```
682
716
 
717
+ ## [Electrical Sensor](./src/dirigera/devices/electrical_sensor.py)
718
+
719
+ Plugs with energy metering, like the GRILLPLATS plug, are reported by the hub as two devices that share the same `relation_id`: an outlet (`isOn`, startup behaviour, ...) and an electrical sensor that holds the metering values.
720
+
721
+ To get the electrical sensors use:
722
+
723
+ ```python
724
+ electrical_sensors = dirigera_hub.get_electrical_sensors()
725
+ ```
726
+
727
+ The electrical sensor object has the following attributes (additional to the core attributes):
728
+
729
+ ```python
730
+ current_active_power: Optional[float] = None
731
+ current_amps: Optional[float] = None
732
+ current_voltage: Optional[float] = None
733
+ energy_consumed_at_last_reset: Optional[float] = None
734
+ total_energy_consumed: Optional[float] = None
735
+ total_energy_consumed_last_updated: Optional[datetime.datetime] = None
736
+ time_of_last_energy_reset: Optional[datetime.datetime] = None
737
+ ```
738
+
739
+ Available methods for electrical sensor are:
740
+
741
+ ```python
742
+ # Reload the electrical sensor data from the hub
743
+ electrical_sensor.reload()
744
+
745
+ # Set the name of the electrical sensor
746
+ electrical_sensor.set_name(name="grill plug")
747
+ ```
748
+
749
+ To find the electrical sensor that belongs to an outlet, compare their `relation_id`:
750
+
751
+ ```python
752
+ outlet = dirigera_hub.get_outlet_by_name(outlet_name="grill plug")
753
+ electrical_sensor = next(
754
+ sensor
755
+ for sensor in dirigera_hub.get_electrical_sensors()
756
+ if sensor.relation_id == outlet.relation_id
757
+ )
758
+ print(electrical_sensor.attributes.current_active_power)
759
+ ```
760
+
683
761
  ## Event Listener
684
762
 
685
763
  The event listener allows you to listen to events that are published by your Dirigera hub. This is useful if you want to automate tasks based on events such as when a light is turned on or off, or when the color temperature of a light is changed.
@@ -692,8 +770,8 @@ from typing import Any
692
770
  def on_message(ws: Any, message: str):
693
771
  message_dict = json.loads(message)
694
772
  data = message_dict["data"]
695
- if data["id"] == bed_light.light_id:
696
- print(f"{message_dict['type']} event on {bed_light.custom_name}, attributes: {data['attributes']}")
773
+ if data["id"] == bed_light.id:
774
+ print(f"{message_dict['type']} event on {bed_light.attributes.custom_name}, attributes: {data['attributes']}")
697
775
 
698
776
  def on_error(ws: Any, message: str):
699
777
  print(message)
@@ -707,6 +785,28 @@ dirigera_hub.create_event_listener(
707
785
  deviceStateChanged event on Bed Light, attributes: {'isOn': False}
708
786
  ```
709
787
 
788
+ ### Reconnecting
789
+
790
+ The hub can close the connection on its own (status 1000, "disconnected due to inactivity."). When that happens, `create_event_listener()` returns. The `reconnect` argument doesn't restart it, since it only covers connection errors and not a clean close by the hub. To keep listening, call it in a loop:
791
+
792
+ ```python
793
+ import time
794
+
795
+ running = True
796
+
797
+ def stop_listening():
798
+ # create_event_listener() blocks, so call this from another thread
799
+ global running
800
+ running = False
801
+ dirigera_hub.stop_event_listener()
802
+
803
+ while running:
804
+ dirigera_hub.create_event_listener(on_message=on_message, on_error=on_error)
805
+ time.sleep(5) # short backoff before listening again
806
+ ```
807
+
808
+ Events that happen while the listener is reconnecting are missed, so re-fetch the device state after reconnecting if you need it (for example in `on_open`).
809
+
710
810
  ## Motivation
711
811
 
712
812
  The primary motivation for this project was to provide users with the ability to control the startup behavior of their smart home lamps when there is a power outage.
@@ -26,6 +26,7 @@ This repository provides an unofficial Python client for controlling the IKEA Di
26
26
  - [Creating a Scene](#creating-a-scene)
27
27
  - [Motion Sensor](#motion-sensor)
28
28
  - [Open Close Sensor](#open-close-sensor)
29
+ - [Electrical Sensor](#electrical-sensor)
29
30
  - [Event Listener](#event-listener)
30
31
  - [Motivation](#motivation)
31
32
  - [Contributing](#contributing)
@@ -96,6 +97,9 @@ response = dirigera_hub.delete(route="/scenes/1")
96
97
  # Fetch all air purifiers registered in the hub
97
98
  air_purifiers = dirigera_hub.get_air_purifiers()
98
99
 
100
+ # Fetch an air purifier by its name
101
+ air_purifier = dirigera_hub.get_air_purifier_by_name(air_purifier_name="living room air purifier")
102
+
99
103
  # Fetch an air purifier by its ID
100
104
  air_purifier = dirigera_hub.get_air_purifier_by_id(id_="1")
101
105
 
@@ -120,6 +124,9 @@ outlet = dirigera_hub.get_outlet_by_id(id_="1")
120
124
  # Fetch all environment sensors registered in the hub
121
125
  sensors = dirigera_hub.get_environment_sensors()
122
126
 
127
+ # Fetch an environment sensor by its name
128
+ sensor = dirigera_hub.get_environment_sensor_by_name(environment_sensor_name="living room environment sensor")
129
+
123
130
  # Fetch an environment sensor by its ID
124
131
  sensor = dirigera_hub.get_environment_sensor_by_id(id_="1")
125
132
 
@@ -135,6 +142,9 @@ sensor = dirigera_hub.get_motion_sensor_by_id(id_="1")
135
142
  # Fetch all open/close sensors registered in the hub
136
143
  sensors = dirigera_hub.get_open_close_sensors()
137
144
 
145
+ # Fetch an open/close sensor by its name
146
+ sensor = dirigera_hub.get_open_close_sensor_by_name(open_close_sensor_name="front door sensor")
147
+
138
148
  # Fetch an open/close sensor by its ID
139
149
  sensor = dirigera_hub.get_open_close_by_id(id_="1")
140
150
 
@@ -162,24 +172,42 @@ scenes = dirigera_hub.get_scenes()
162
172
  # Fetch a scene by its ID
163
173
  scene = dirigera_hub.get_scene_by_id(scene_id="1")
164
174
 
175
+ # Fetch a scene by its name
176
+ scene = dirigera_hub.get_scene_by_name(scene_name="movie night")
177
+
165
178
  # Fetch all water sensors registered in the hub
166
179
  water_sensors = dirigera_hub.get_water_sensors()
167
180
 
181
+ # Fetch a water sensor by its name
182
+ water_sensor = dirigera_hub.get_water_sensor_by_name(water_sensor_name="bathroom water sensor")
183
+
168
184
  # Fetch a water sensor by its ID
169
185
  water_sensor = dirigera_hub.get_water_sensor_by_id(id_="1")
170
186
 
171
187
  # Fetch all occupancy sensors registered in the hub
172
188
  occupancy_sensors = dirigera_hub.get_occupancy_sensors()
173
189
 
190
+ # Fetch an occupancy sensor by its name
191
+ occupancy_sensor = dirigera_hub.get_occupancy_sensor_by_name(occupancy_sensor_name="hallway occupancy sensor")
192
+
174
193
  # Fetch an occupancy sensor by its ID
175
194
  occupancy_sensor = dirigera_hub.get_occupancy_sensor_by_id(id_="1")
176
195
 
177
196
  # Fetch all light sensors registered in the hub
178
197
  light_sensors = dirigera_hub.get_light_sensors()
179
198
 
199
+ # Fetch a light sensor by its name
200
+ light_sensor = dirigera_hub.get_light_sensor_by_name(light_sensor_name="living room light sensor")
201
+
180
202
  # Fetch a light sensor by its ID
181
203
  light_sensor = dirigera_hub.get_light_sensor_by_id(id_="1")
182
204
 
205
+ # Fetch all electrical sensors registered in the hub
206
+ electrical_sensors = dirigera_hub.get_electrical_sensors()
207
+
208
+ # Fetch an electrical sensor by its ID
209
+ electrical_sensor = dirigera_hub.get_electrical_sensor_by_id(id_="1")
210
+
183
211
  # Fetch all devices registered in the hub
184
212
  devices = dirigera_hub.get_all_devices()
185
213
 
@@ -199,7 +227,8 @@ dirigera_hub.delete_scene(scene_id="1")
199
227
 
200
228
  # [Devices](./src/dirigera/devices/device.py)
201
229
 
202
- All available devices (Light, Controller, Outlet, ...) consist of the core data defined in [device.py](./src/dirigera/devices/device.py):
230
+ All available devices (Light, Controller, Outlet, ...) consist of the core data defined in [device.py](./src/dirigera/devices/device.py).
231
+ Calling `reload()` on a device (or scene) fetches the current data from the hub and updates the object in place. It also returns the same object, so `light = light.reload()` works as well.
203
232
 
204
233
  ### Core Device Data
205
234
 
@@ -276,7 +305,7 @@ light_level: Optional[int] = None
276
305
  color_temperature: Optional[int] = None
277
306
  color_temperature_min: Optional[int] = None
278
307
  color_temperature_max: Optional[int] = None
279
- color_hue: Optional[int] = None
308
+ color_hue: Optional[float] = None
280
309
  color_saturation: Optional[float] = None
281
310
  ```
282
311
 
@@ -333,7 +362,7 @@ time_of_last_energy_reset: Optional[datetime.datetime] = None
333
362
  Available methods for outlet are:
334
363
 
335
364
  ```python
336
- # Reload the air outlet data from the hub
365
+ # Reload the outlet data from the hub
337
366
  outlet.reload()
338
367
 
339
368
  # Set the name of the outlet
@@ -369,7 +398,7 @@ filter_lifetime: int
369
398
  current_p_m25: int
370
399
  ```
371
400
 
372
- Available methods for blinds are:
401
+ Available methods for air purifier are:
373
402
 
374
403
  ```python
375
404
  # Reload the air purifier data from the hub
@@ -419,6 +448,9 @@ blind.set_name(name="kitchen blind 1")
419
448
 
420
449
  # Set the target level of the blind (0-100)
421
450
  blind.set_target_level(target_level=90)
451
+
452
+ # Move the blind up or down, or stop it at its current position
453
+ blind.set_state(state=BlindsStateEnum.STOPPED)
422
454
  ```
423
455
 
424
456
  ## [Remote Controllers](./src/dirigera/devices/controller.py)
@@ -501,7 +533,7 @@ created_at: datetime.datetime
501
533
  last_completed: Optional[datetime.datetime] = None
502
534
  last_triggered: Optional[datetime.datetime] = None
503
535
  last_undo: Optional[datetime.datetime] = None
504
- commands: List[str]
536
+ commands: List[Union[str, Dict[str, Any]]]
505
537
  undo_allowed_duration: int
506
538
  ```
507
539
 
@@ -585,7 +617,7 @@ All available icons can be found here: [Icons](./src/dirigera/devices/scene.py)
585
617
  To get information about the available motion sensors, you can use the `get_motion_sensors()` method:
586
618
 
587
619
  ```python
588
- motions_sensors = dirigera_hub.get_motion_sensors()
620
+ motion_sensors = dirigera_hub.get_motion_sensors()
589
621
  ```
590
622
 
591
623
  The motion sensor object has the following attributes (additional to the core attributes):
@@ -597,7 +629,7 @@ light_level: Optional[float] = None
597
629
  is_detected: Optional[bool] = False
598
630
  ```
599
631
 
600
- Available methods for outlet are:
632
+ Available methods for motion sensor are:
601
633
 
602
634
  ```python
603
635
  # Reload the motion sensor data from the hub
@@ -615,6 +647,8 @@ To get information about the available open/close sensors, you can use the `get_
615
647
  open_close_sensors = dirigera_hub.get_open_close_sensors()
616
648
  ```
617
649
 
650
+ The hub does not guarantee the order of the returned devices, so look sensors up by their id (`get_open_close_by_id()`) or name (`get_open_close_sensor_by_name()`) instead of by their position in the list.
651
+
618
652
  The open_close_sensor object has the following attributes (additional to the core attributes):
619
653
 
620
654
  ```python
@@ -622,7 +656,7 @@ is_open: bool
622
656
  battery_percentage: Optional[int] = None
623
657
  ```
624
658
 
625
- Available methods for outlet are:
659
+ Available methods for open/close sensor are:
626
660
 
627
661
  ```python
628
662
  # Reload the open/close sensor data from the hub
@@ -632,6 +666,50 @@ open_close_sensor.reload()
632
666
  open_close_sensor.set_name(name="window 1")
633
667
  ```
634
668
 
669
+ ## [Electrical Sensor](./src/dirigera/devices/electrical_sensor.py)
670
+
671
+ Plugs with energy metering, like the GRILLPLATS plug, are reported by the hub as two devices that share the same `relation_id`: an outlet (`isOn`, startup behaviour, ...) and an electrical sensor that holds the metering values.
672
+
673
+ To get the electrical sensors use:
674
+
675
+ ```python
676
+ electrical_sensors = dirigera_hub.get_electrical_sensors()
677
+ ```
678
+
679
+ The electrical sensor object has the following attributes (additional to the core attributes):
680
+
681
+ ```python
682
+ current_active_power: Optional[float] = None
683
+ current_amps: Optional[float] = None
684
+ current_voltage: Optional[float] = None
685
+ energy_consumed_at_last_reset: Optional[float] = None
686
+ total_energy_consumed: Optional[float] = None
687
+ total_energy_consumed_last_updated: Optional[datetime.datetime] = None
688
+ time_of_last_energy_reset: Optional[datetime.datetime] = None
689
+ ```
690
+
691
+ Available methods for electrical sensor are:
692
+
693
+ ```python
694
+ # Reload the electrical sensor data from the hub
695
+ electrical_sensor.reload()
696
+
697
+ # Set the name of the electrical sensor
698
+ electrical_sensor.set_name(name="grill plug")
699
+ ```
700
+
701
+ To find the electrical sensor that belongs to an outlet, compare their `relation_id`:
702
+
703
+ ```python
704
+ outlet = dirigera_hub.get_outlet_by_name(outlet_name="grill plug")
705
+ electrical_sensor = next(
706
+ sensor
707
+ for sensor in dirigera_hub.get_electrical_sensors()
708
+ if sensor.relation_id == outlet.relation_id
709
+ )
710
+ print(electrical_sensor.attributes.current_active_power)
711
+ ```
712
+
635
713
  ## Event Listener
636
714
 
637
715
  The event listener allows you to listen to events that are published by your Dirigera hub. This is useful if you want to automate tasks based on events such as when a light is turned on or off, or when the color temperature of a light is changed.
@@ -644,8 +722,8 @@ from typing import Any
644
722
  def on_message(ws: Any, message: str):
645
723
  message_dict = json.loads(message)
646
724
  data = message_dict["data"]
647
- if data["id"] == bed_light.light_id:
648
- print(f"{message_dict['type']} event on {bed_light.custom_name}, attributes: {data['attributes']}")
725
+ if data["id"] == bed_light.id:
726
+ print(f"{message_dict['type']} event on {bed_light.attributes.custom_name}, attributes: {data['attributes']}")
649
727
 
650
728
  def on_error(ws: Any, message: str):
651
729
  print(message)
@@ -659,6 +737,28 @@ dirigera_hub.create_event_listener(
659
737
  deviceStateChanged event on Bed Light, attributes: {'isOn': False}
660
738
  ```
661
739
 
740
+ ### Reconnecting
741
+
742
+ The hub can close the connection on its own (status 1000, "disconnected due to inactivity."). When that happens, `create_event_listener()` returns. The `reconnect` argument doesn't restart it, since it only covers connection errors and not a clean close by the hub. To keep listening, call it in a loop:
743
+
744
+ ```python
745
+ import time
746
+
747
+ running = True
748
+
749
+ def stop_listening():
750
+ # create_event_listener() blocks, so call this from another thread
751
+ global running
752
+ running = False
753
+ dirigera_hub.stop_event_listener()
754
+
755
+ while running:
756
+ dirigera_hub.create_event_listener(on_message=on_message, on_error=on_error)
757
+ time.sleep(5) # short backoff before listening again
758
+ ```
759
+
760
+ Events that happen while the listener is reconnecting are missed, so re-fetch the device state after reconnecting if you need it (for example in `on_open`).
761
+
662
762
  ## Motivation
663
763
 
664
764
  The primary motivation for this project was to provide users with the ability to control the startup behavior of their smart home lamps when there is a power outage.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dirigera"
7
- version = "1.2.7"
7
+ version = "1.3.0"
8
8
  description = "An unofficial Python client for controlling the IKEA Dirigera Smart Home Hub"
9
9
  readme = "README.md"
10
10
  authors = [{ name = "Leggin", email = "legginsun@gmail.com" }]
@@ -32,7 +32,8 @@ class AirPurifier(Device):
32
32
 
33
33
  def reload(self) -> AirPurifier:
34
34
  data = self.dirigera_client.get(route=f"/devices/{self.id}")
35
- return AirPurifier(dirigeraClient=self.dirigera_client, **data)
35
+ self._copy_from(AirPurifier(dirigeraClient=self.dirigera_client, **data))
36
+ return self
36
37
 
37
38
  def set_name(self, name: str) -> None:
38
39
  if "customName" not in self.capabilities.can_receive:
@@ -7,4 +7,7 @@ def to_camel(string: str) -> str:
7
7
 
8
8
 
9
9
  class BaseIkeaModel(BaseModel, arbitrary_types_allowed=True, alias_generator=to_camel):
10
- pass
10
+ def _copy_from(self, other: "BaseIkeaModel") -> None:
11
+ """Overwrite all fields of this instance with the values of another one."""
12
+ for name in type(self).model_fields:
13
+ setattr(self, name, getattr(other, name))
@@ -1,9 +1,16 @@
1
1
  from __future__ import annotations
2
+ from enum import Enum
2
3
  from typing import Any, Optional, Dict
3
4
  from .device import Attributes, Device
4
5
  from ..hub.abstract_smart_home_hub import AbstractSmartHomeHub
5
6
 
6
7
 
8
+ class BlindsStateEnum(Enum):
9
+ UP = "up"
10
+ DOWN = "down"
11
+ STOPPED = "stopped"
12
+
13
+
7
14
  class BlindAttributes(Attributes):
8
15
  blinds_current_level: Optional[int] = None
9
16
  blinds_target_level: Optional[int] = None
@@ -17,7 +24,8 @@ class Blind(Device):
17
24
 
18
25
  def reload(self) -> Blind:
19
26
  data = self.dirigera_client.get(route=f"/devices/{self.id}")
20
- return Blind(dirigeraClient=self.dirigera_client, **data)
27
+ self._copy_from(Blind(dirigeraClient=self.dirigera_client, **data))
28
+ return self
21
29
 
22
30
  def set_name(self, name: str) -> None:
23
31
  if "customName" not in self.capabilities.can_receive:
@@ -40,6 +48,14 @@ class Blind(Device):
40
48
  self.dirigera_client.patch(route=f"/devices/{self.id}", data=data)
41
49
  self.attributes.blinds_target_level = target_level
42
50
 
51
+ def set_state(self, state: BlindsStateEnum) -> None:
52
+ if "blindsState" not in self.capabilities.can_receive:
53
+ raise AssertionError("This blind does not support the state function")
54
+
55
+ data = [{"attributes": {"blindsState": state.value}}]
56
+ self.dirigera_client.patch(route=f"/devices/{self.id}", data=data)
57
+ self.attributes.blinds_state = state.value
58
+
43
59
 
44
60
  def dict_to_blind(data: Dict[str, Any], dirigera_client: AbstractSmartHomeHub) -> Blind:
45
61
  return Blind(dirigeraClient=dirigera_client, **data)
@@ -16,7 +16,8 @@ class Controller(Device):
16
16
 
17
17
  def reload(self) -> Controller:
18
18
  data = self.dirigera_client.get(route=f"/devices/{self.id}")
19
- return Controller(dirigeraClient=self.dirigera_client, **data)
19
+ self._copy_from(Controller(dirigeraClient=self.dirigera_client, **data))
20
+ return self
20
21
 
21
22
  def set_name(self, name: str) -> None:
22
23
  if "customName" not in self.capabilities.can_receive:
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
  import datetime
3
3
  from enum import Enum
4
- from typing import Any, Dict, Optional, List
4
+ from typing import Optional, List
5
5
  from .base_ikea_model import BaseIkeaModel
6
6
 
7
7
 
@@ -54,6 +54,3 @@ class Device(BaseIkeaModel):
54
54
  device_set: List
55
55
  remote_links: List[str]
56
56
  is_hidden: Optional[bool] = None
57
-
58
- def _reload(self, data: Dict[str, Any]) -> Device:
59
- return Device(**data)
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+ import datetime
3
+ from typing import Any, Dict, Optional
4
+ from .device import Attributes, Device
5
+ from ..hub.abstract_smart_home_hub import AbstractSmartHomeHub
6
+
7
+
8
+ class ElectricalSensorAttributes(Attributes):
9
+ current_active_power: Optional[float] = None
10
+ current_amps: Optional[float] = None
11
+ current_voltage: Optional[float] = None
12
+ energy_consumed_at_last_reset: Optional[float] = None
13
+ total_energy_consumed: Optional[float] = None
14
+ total_energy_consumed_last_updated: Optional[datetime.datetime] = None
15
+ time_of_last_energy_reset: Optional[datetime.datetime] = None
16
+
17
+
18
+ class ElectricalSensor(Device):
19
+ dirigera_client: AbstractSmartHomeHub
20
+ attributes: ElectricalSensorAttributes
21
+
22
+ def reload(self) -> ElectricalSensor:
23
+ data = self.dirigera_client.get(route=f"/devices/{self.id}")
24
+ self._copy_from(ElectricalSensor(dirigeraClient=self.dirigera_client, **data))
25
+ return self
26
+
27
+ def set_name(self, name: str) -> None:
28
+ if "customName" not in self.capabilities.can_receive:
29
+ raise AssertionError("This sensor does not support the set_name function")
30
+
31
+ data = [{"attributes": {"customName": name}}]
32
+ self.dirigera_client.patch(route=f"/devices/{self.id}", data=data)
33
+ self.attributes.custom_name = name
34
+
35
+
36
+ def dict_to_electrical_sensor(
37
+ data: Dict[str, Any], dirigera_client: AbstractSmartHomeHub
38
+ ) -> ElectricalSensor:
39
+ return ElectricalSensor(dirigeraClient=dirigera_client, **data)
@@ -24,7 +24,8 @@ class EnvironmentSensor(Device):
24
24
 
25
25
  def reload(self) -> EnvironmentSensor:
26
26
  data = self.dirigera_client.get(route=f"/devices/{self.id}")
27
- return EnvironmentSensor(dirigeraClient=self.dirigera_client, **data)
27
+ self._copy_from(EnvironmentSensor(dirigeraClient=self.dirigera_client, **data))
28
+ return self
28
29
 
29
30
  def set_name(self, name: str) -> None:
30
31
  if "customName" not in self.capabilities.can_receive: