|
1 | | -Python library to provide reliable communication with Lightwave Smart Series (second generation) devices. Including lights (dimmers), power outlets (sockets), smart switchs (wirefrees), PIRs, thermostats, TRVs, magnetic switches, relays, energy monitors and other device types. |
| 1 | +# Lightwave Smart Python Library |
2 | 2 |
|
3 | | -## Notes |
4 | | -**The LWLink2Public class should not be used in this version, it has not been tested.** |
| 3 | +[](https://badge.fury.io/py/lightwave_smart) |
| 4 | +[](https://www.python.org/downloads/) |
| 5 | +[](https://opensource.org/licenses/MIT) |
5 | 6 |
|
6 | | -## Installing |
| 7 | +A Python library for controlling Lightwave (https://lightwaverf.com) Smart Series (second generation) devices as well as Connect Series (first generation) devices connected to a Link Plus hub. |
| 8 | +Control and monitor lights (dimmers), power outlets (sockets), smart switches (wirefrees), PIRs, thermostats, TRVs, magnetic switches, relays, energy monitors and other device types. |
7 | 9 |
|
8 | | -The easiest way is |
| 10 | +Supported devices include: |
| 11 | ++ [Link Plus hub](https://shop.lightwaverf.com/collections/all/products/link-plus) (required) |
| 12 | ++ [Light Switches / Dimmers](https://shop.lightwaverf.com/collections/smart-lighting) |
| 13 | ++ [Power Outlets](https://shop.lightwaverf.com/collections/smart-sockets) |
| 14 | ++ [Smart Switches](https://shop.lightwaverf.com/collections/scene-selectors) |
| 15 | ++ [TRVs and Thermostats](https://shop.lightwaverf.com/collections/smart-heating) |
| 16 | ++ [Relays and LED Drivers](https://shop.lightwaverf.com/collections/relays-and-led-drivers) |
| 17 | ++ And more... |
9 | 18 |
|
10 | | - pip3 install lightwave_smart |
| 19 | +## Updates |
11 | 20 |
|
12 | | -Or just copy https://raw.githubusercontent.com/LightwaveSmartHome/lightwave-smart/master/lightwave_smart/lightwave_smart.py into your project |
| 21 | +**Important**: The `LWLink2Public` class has been removed as of version 1.0.0. |
13 | 22 |
|
14 | | -## Using the library |
| 23 | +## Installation |
15 | 24 |
|
16 | | -### Imports |
17 | | -You'll need to import the library |
| 25 | +```bash |
| 26 | +pip install lightwave_smart |
| 27 | +``` |
18 | 28 |
|
19 | | - from lightwave_smart import lightwave_smart |
| 29 | +## Quick Start |
20 | 30 |
|
21 | | -If you want to see all the messages passed back and forth with the Lightwave servers, set the logging level to debug: |
| 31 | +```python |
| 32 | +import asyncio |
| 33 | +from lightwave_smart import lightwave_smart |
22 | 34 |
|
23 | | - import logging |
24 | | - logging.basicConfig(level=logging.DEBUG) |
| 35 | +async def main(): |
| 36 | + # Create and authenticate |
| 37 | + link = lightwave_smart.LWLink2() |
| 38 | + link.auth.set_auth_method(auth_method="password", username="your_email@example.com", password="your_password") |
| 39 | + await link.async_activate() |
25 | 40 |
|
26 | | -### Connecting |
27 | | -Start by authenticating with the LW servers. |
28 | | - |
29 | | - link = lightwave_smart.LWLink2("example@example.com", "password") |
| 41 | + # Get devices and feature sets |
| 42 | + await link.async_get_hierarchy() |
30 | 43 |
|
31 | | -This sets up a `LWLink2` object called `link`, and gets an authentication token from LW which is stored in the object. We can now connect to the LW websocket service |
32 | | - |
33 | | - link.connect() |
34 | | - |
35 | | -### Read hierarchy |
36 | | -Next: |
37 | | - |
38 | | - link.get_hierarchy() |
39 | | - |
40 | | -This requests the LW server to tell us all of the registered "featuresets". A "featureset" is LW's word for a group of features (e.g. a light switch could have features for "power" and "brightness") - this is what I think of as a device, but that's not how LW describes them (sidenote: what LW considers to be a device depends on the generation of the hardware - for gen 1 hardware, devices and featuresets correspond, for gen2 a device corresponds to a physical object; e.g. a 2 gang switch is a single device, but 2 featuresets). |
41 | | - |
42 | | -Running `get_hierarchy` populates a dictionary of all of the featuresets available. the dictionary keys are unique identifiers provided by LW, the values are `LWRFFeatureSet` objects that hold information about the featureset. |
43 | | - |
44 | | -To see the objects: |
45 | | - |
46 | | - print(link.featuresets) |
| 44 | + # List feature sets |
| 45 | + for featureset in link.featuresets.values(): |
| 46 | + device = featureset.device |
| 47 | + print(f"{featureset.name} (ID: {featureset.featureset_id}), Device Product Code: {device.product_code}") |
47 | 48 |
|
48 | | -For a slightly more useful view: |
| 49 | + # Control a light |
| 50 | + featureset_id = "your-feature-set-id" |
| 51 | + await link.async_turn_on_by_featureset_id(featureset_id) |
| 52 | + await link.async_set_brightness_by_featureset_id(featureset_id, 75) |
49 | 53 |
|
50 | | - for i in link.featuresets.values(): |
51 | | - print(i.name, i.featureset_id, i.features) |
52 | | - |
53 | | -In my case this returns |
| 54 | + await link.async_deactivate() |
54 | 55 |
|
55 | | - Garden Room 5bc4d06e87779374d29d7d9a-5bc4d61387779374d29fdd1e {'switch': <lightwave_smart.lightwave_smart.LWRFFeature object at 0x0000021DB49C93A0>, 'protection': <lightwave_smart.lightwave_smart.LWRFFeature object at 0x0000021DB49C9AC0>, 'dimLevel': <lightwave_smart.lightwave_smart.LWRFFeature object at 0x0000021DB49C9B50>, 'identify': <lightwave_smart.lightwave_smart.LWRFFeature object at 0x0000021DB49C9BB0>} |
| 56 | +asyncio.run(main()) |
| 57 | +``` |
56 | 58 |
|
57 | | -This is a light switch with the name `Garden Room` and the featureset id `5bc4d06e87779374d29d7d9a-5bc4d61387779374d29fdd1e` which we'll use in the example. The features will be explained below. |
| 59 | +## Key Concepts |
58 | 60 |
|
59 | | -### Reading the featuresets |
| 61 | +- **Devices**: Physical devices (light switches, thermostats, etc.) |
| 62 | +- **Feature Sets**: Logical groupings within a device (e.g. a 2-circuit switch has 2 feature sets) |
60 | 63 |
|
61 | | -Featuresets are accessed from the dictionary directly: |
62 | | - |
63 | | -##### Name |
64 | | - print(link.featuresets['5bc4d06e87779374d29d7d9a-5bc4d61387779374d29fdd1e'].name) |
65 | | - |
66 | | -will give the name you assigned when you set up the device in the LW app. |
| 64 | +## Device Control |
67 | 65 |
|
68 | | -##### Type of device |
| 66 | +### Basic Control |
| 67 | +```python |
| 68 | +# Lights/Switches |
| 69 | +await link.async_turn_on_by_featureset_id(featureset_id) |
| 70 | +await link.async_turn_off_by_featureset_id(featureset_id) |
| 71 | +await link.async_set_brightness_by_featureset_id(featureset_id, 50) |
69 | 72 |
|
70 | | -There are a number of methods to return info about the devices |
| 73 | +# Thermostats |
| 74 | +await link.async_set_temperature_by_featureset_id(featureset_id, 22.5) |
71 | 75 |
|
72 | | -|Method|Usage| |
73 | | -|---|---| |
74 | | -|print(link.featuresets['5bc4d06e87779374d29d7d9a-5bc4d61387779374d29fdd1e'].is_switch())|Is it a socket?| |
75 | | -|print(link.featuresets['5bc4d06e87779374d29d7d9a-5bc4d61387779374d29fdd1e'].is_light())|Light switches| |
76 | | -|print(link.featuresets['5bc4d06e87779374d29d7d9a-5bc4d61387779374d29fdd1e'].is_climate())|Thermostats| |
77 | | -|print(link.featuresets['5bc4d06e87779374d29d7d9a-5bc4d61387779374d29fdd1e'].is_cover())|Blinds / three-way relay| |
78 | | -|print(link.featuresets['5bc4d06e87779374d29d7d9a-5bc4d61387779374d29fdd1e'].is_climate())|Thermostats| |
79 | | -|print(link.featuresets['5bc4d06e87779374d29d7d9a-5bc4d61387779374d29fdd1e'].is_energy())|Energy meters| |
80 | | -|print(link.featuresets['5bc4d06e87779374d29d7d9a-5bc4d61387779374d29fdd1e'].is_windowsensor())|Window sensor| |
81 | | -|print(link.featuresets['5bc4d06e87779374d29d7d9a-5bc4d61387779374d29fdd1e'].is_hub())|LinkPlus Hub| |
82 | | -|print(link.featuresets['5bc4d06e87779374d29d7d9a-5bc4d61387779374d29fdd1e'].is_gen2())|Generation 2 device?| |
83 | | -|print(link.featuresets['5bc4d06e87779374d29d7d9a-5bc4d61387779374d29fdd1e'].reports_power())|Has power reporting| |
84 | | -|print(link.featuresets['5bc4d06e87779374d29d7d9a-5bc4d61387779374d29fdd1e'].has_led())|Has an indicator LED that is configurable| |
| 76 | +# Covers/Blinds |
| 77 | +await link.async_cover_open_by_featureset_id(featureset_id) |
| 78 | +await link.async_cover_close_by_featureset_id(featureset_id) |
| 79 | +``` |
85 | 80 |
|
86 | | -##### Device features |
| 81 | +### Device Type Detection |
| 82 | +```python |
| 83 | +device = link.featuresets['featureset-id'].device |
| 84 | +print(f"Is light: {device.is_light()}") |
| 85 | +print(f"Is climate: {device.is_climate()}") |
| 86 | +print(f"Is switch: {device.is_switch()}") |
| 87 | +``` |
87 | 88 |
|
88 | | -This is how we find out the state of the device, and we will also use this information to control the device: |
| 89 | +### Event Callbacks |
| 90 | +```python |
| 91 | +def feature_changed(**kwargs): |
| 92 | + # example output for a switch: |
| 93 | + # {'feature': 'switch', 'feature_id': 'your-feature-set-id', 'prev_value': 0, 'new_value': 1} |
| 94 | + print(f"Feature changed: {kwargs}") |
89 | 95 |
|
90 | | - print(link.featuresets['5bc4d06e87779374d29d7d9a-5bc4d61387779374d29fdd1e'].features) |
| 96 | +# Register callback |
| 97 | +await link.async_register_feature_callback(featureset_id, feature_changed) |
| 98 | +``` |
91 | 99 |
|
92 | | -`features` is a dictionary of the features within a given featureset. The keys are the names of the features, the values are LWRFFeature objects. |
| 100 | +## API Reference |
93 | 101 |
|
94 | | -The LWRFFeature objects have properties: featureset, id, name, state. E.g. |
| 102 | +### Core Methods |
| 103 | +- `async_activate()` - Connect to Lightwave servers |
| 104 | +- `async_get_hierarchy()` - Get all devices and feature sets, reads and updates all features states in the background |
95 | 105 |
|
96 | | - for i in link.featuresets['5bc4d06e87779374d29d7d9a-5bc4d61387779374d29fdd1e'].features.values(): |
97 | | - print(i.name, i.id, i.state) |
| 106 | +After calling `async_get_hierarchy` all feature known at that time will have their states updated as they change based on events received from the websocket. |
98 | 107 |
|
99 | | -returns |
| 108 | +### Device Control |
| 109 | +- `async_turn_on_by_featureset_id(id)` - Turn device on |
| 110 | +- `async_turn_off_by_featureset_id(id)` - Turn device off |
| 111 | +- `async_set_brightness_by_featureset_id(id, level)` - Set brightness (0-100) |
| 112 | +- `async_set_temperature_by_featureset_id(id, temp)` - Set temperature |
100 | 113 |
|
101 | | - switch 5bc4d06e87779374d29d7d9a-28-3157334318+1 0 |
102 | | - protection 5bc4d06e87779374d29d7d9a-29-3157334318+1 0 |
103 | | - dimLevel 5bc4d06e87779374d29d7d9a-30-3157334318+1 59 |
104 | | - identify 5bc4d06e87779374d29d7d9a-72-3157334318+1 0 |
| 114 | +### Device Information |
| 115 | +- `is_light()`, `is_climate()`, `is_switch()`, `is_cover()`, `is_energy()` - Device type checks |
| 116 | +- `is_gen2()`, `is_hub()`, `is_trv()` - Specific device checks |
105 | 117 |
|
106 | | -showing the light is currently off (feature `switch`), the physical buttons are not locked (feature `protection`) and the brightness is set to 59% (feature `dimlevel`). |
107 | | - |
108 | | -#### More reading the featuresets |
109 | | - |
110 | | -The values of the featuresets are static and won't respond to changes in the state of the physical device (unless you set up a callback to handle messages from the server). If you want to make sure the values are up to date you can: |
111 | | - |
112 | | - link.update_featureset_states() |
113 | | - |
114 | | -Finally there are a handful of convenience methods if you just want to return devices of a particular type: |
115 | | - |
116 | | - print(link.get_switches()) |
117 | | - print(link.get_lights()) |
118 | | - print(link.get_climates()) |
119 | | - print(link.get_energy()) |
120 | | - |
121 | | -#### Writing to a feature |
122 | | -Turning on a switch/light, turning off a switch/light or setting the brightness level for a light is as follows: |
123 | | - |
124 | | - link.turn_on_by_featureset_id("5bc4d06e87779374d29d7d9a-5bc4d61387779374d29fdd1e") |
125 | | - link.turn_off_by_featureset_id("5bc4d06e87779374d29d7d9a-5bc4d61387779374d29fdd1e") |
126 | | - link.set_brightness_by_featureset_id("5bc4d06e87779374d29d7d9a-5bc4d61387779374d29fdd1e", 60) #Brightness in percent |
127 | | - |
128 | | -Then there is one more method for a thermostat |
129 | | - |
130 | | - link.set_temperature_by_featureset_id(featureset_id, level) |
| 118 | +## Examples |
131 | 119 |
|
132 | | -And some methods for covers (blinds) |
133 | | - |
134 | | - link.cover_open_by_featureset_id(featureset_id) |
135 | | - link.cover_close_by_featureset_id(featureset_id) |
136 | | - link.cover_stop_by_featureset_id(featureset_id) |
137 | | - |
138 | | -#### Reading/writing to an arbitrary feature |
139 | | - |
140 | | -Finally, for any other features you might want to read or write the value of, you can access them directly. Note that the first option needs the **feature** unique id. |
141 | | - |
142 | | - link.read_feature(feature_id) |
143 | | - |
144 | | -or |
145 | | - |
146 | | - link.featuresets[featureset_id].features[featurename].state |
147 | | - |
148 | | -Writing: |
149 | | - |
150 | | - link.write_feature(feature_id, value) |
151 | | - |
152 | | -or |
153 | | - |
154 | | - link.write_feature_by_name(featureset_id, featurename, value) |
155 | | - |
156 | | -or |
157 | | - |
158 | | - await link.featuresets[featureset_id].features[featurename].set_state(value) #async only, see below |
159 | | - |
160 | | -#### Getting notified when something changes |
161 | | - |
162 | | -This library is all using async programming, so notifications will only really work if your code is also async and is managing the event loop. Nonetheless, you can try the following for an idea of how to get a callback when an event is spotted by the server: |
163 | | - |
164 | | - import asyncio |
165 | | - |
166 | | - def test(): |
167 | | - print("this is a test callback") |
168 | | - |
169 | | - asyncio.get_event_loop().run_until_complete(link.async_register_callback(test)) |
170 | | - |
171 | | -This will call the `test` function every time a change is detected to the state of one of the features. This is likely only useful if you then run `link.update_featureset_states()` to ensure the internal state of the object is consistent with your actual LW system. |
| 120 | +- `example_readme.py` - Basic synchronous usage |
| 121 | +- `example_async.py` - Advanced async usage with callbacks |
172 | 122 |
|
173 | | -See example_async.py for a minimal client. |
| 123 | +## Contributing |
174 | 124 |
|
175 | | -#### async methods |
| 125 | +Contributions welcome! Fork, create a feature branch, commit changes, and submit a PR. |
176 | 126 |
|
177 | | -The library is actually all built on async methods (the sync versions described above are just wrappers for the async versions) |
| 127 | +## License |
178 | 128 |
|
179 | | - async_connect() |
180 | | - async_get_hierarchy() |
181 | | - async_update_featureset_states() |
182 | | - async_write_feature(feature_id, value) |
183 | | - async_read_feature(feature_id) |
184 | | - async_turn_on_by_featureset_id(featureset_id) |
185 | | - async_turn_off_by_featureset_id(featureset_id) |
186 | | - async_set_brightness_by_featureset_id(featureset_id, level) |
187 | | - async_set_temperature_by_featureset_id(featureset_id, level) |
| 129 | +MIT License - see [LICENSE](LICENSE) file. |
188 | 130 |
|
189 | 131 | ## Thanks |
190 | 132 |
|
191 | | -Credit to Bryan Blunt for the original version https://github.com/bigbadblunt/lightwave2 |
| 133 | +Credit to Bryan Blunt for the original version: https://github.com/bigbadblunt/lightwave2 |
0 commit comments