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.
- {dirigera-1.2.7/src/dirigera.egg-info → dirigera-1.3.0}/PKG-INFO +111 -11
- {dirigera-1.2.7 → dirigera-1.3.0}/README.md +110 -10
- {dirigera-1.2.7 → dirigera-1.3.0}/pyproject.toml +1 -1
- {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/devices/air_purifier.py +2 -1
- {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/devices/base_ikea_model.py +4 -1
- {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/devices/blinds.py +17 -1
- {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/devices/controller.py +2 -1
- {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/devices/device.py +1 -4
- dirigera-1.3.0/src/dirigera/devices/electrical_sensor.py +39 -0
- {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/devices/environment_sensor.py +2 -1
- {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/devices/light.py +18 -7
- {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/devices/light_sensor.py +2 -1
- {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/devices/motion_sensor.py +2 -1
- {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/devices/occupancy_sensor.py +2 -1
- {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/devices/open_close_sensor.py +2 -1
- {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/devices/outlet.py +2 -1
- {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/devices/scene.py +44 -5
- {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/devices/water_sensor.py +2 -1
- {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/hub/hub.py +95 -3
- {dirigera-1.2.7 → dirigera-1.3.0/src/dirigera.egg-info}/PKG-INFO +111 -11
- {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera.egg-info/SOURCES.txt +4 -0
- {dirigera-1.2.7 → dirigera-1.3.0}/tests/test_blinds.py +18 -1
- dirigera-1.3.0/tests/test_electrical_sensor.py +94 -0
- dirigera-1.3.0/tests/test_hub.py +93 -0
- dirigera-1.3.0/tests/test_hub_get_by_name.py +156 -0
- {dirigera-1.2.7 → dirigera-1.3.0}/tests/test_light.py +68 -0
- {dirigera-1.2.7 → dirigera-1.3.0}/tests/test_outlet.py +19 -0
- {dirigera-1.2.7 → dirigera-1.3.0}/tests/test_scenes.py +130 -3
- {dirigera-1.2.7 → dirigera-1.3.0}/LICENSE +0 -0
- {dirigera-1.2.7 → dirigera-1.3.0}/setup.cfg +0 -0
- {dirigera-1.2.7 → dirigera-1.3.0}/setup.py +0 -0
- {dirigera-1.2.7 → dirigera-1.3.0}/src/__init__.py +0 -0
- {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/__init__.py +0 -0
- {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/devices/__init__.py +0 -0
- {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/hub/__init__.py +0 -0
- {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/hub/abstract_smart_home_hub.py +0 -0
- {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/hub/auth.py +0 -0
- {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera/hub/utils.py +0 -0
- {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera.egg-info/dependency_links.txt +0 -0
- {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera.egg-info/entry_points.txt +0 -0
- {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera.egg-info/requires.txt +0 -0
- {dirigera-1.2.7 → dirigera-1.3.0}/src/dirigera.egg-info/top_level.txt +0 -0
- {dirigera-1.2.7 → dirigera-1.3.0}/tests/test_air_purifier.py +0 -0
- {dirigera-1.2.7 → dirigera-1.3.0}/tests/test_controller.py +0 -0
- {dirigera-1.2.7 → dirigera-1.3.0}/tests/test_environment_sensor.py +0 -0
- {dirigera-1.2.7 → dirigera-1.3.0}/tests/test_light_sensor.py +0 -0
- {dirigera-1.2.7 → dirigera-1.3.0}/tests/test_motion_sensor.py +0 -0
- {dirigera-1.2.7 → dirigera-1.3.0}/tests/test_occupancy_sensor.py +0 -0
- {dirigera-1.2.7 → dirigera-1.3.0}/tests/test_open_close_sensor.py +0 -0
- {dirigera-1.2.7 → dirigera-1.3.0}/tests/test_utils.py +0 -0
- {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.
|
|
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[
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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[
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|