Skip to content

Commit d476139

Browse files
committed
remove LWLink2Public class
remove class initiation params for credentials on LWLink2 improve activation and deactivation
1 parent 0c64843 commit d476139

File tree

10 files changed

+449
-555
lines changed

10 files changed

+449
-555
lines changed

README.md

Lines changed: 95 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -1,191 +1,133 @@
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
22

3-
## Notes
4-
**The LWLink2Public class should not be used in this version, it has not been tested.**
3+
[![PyPI version](https://badge.fury.io/py/lightwave_smart.svg)](https://badge.fury.io/py/lightwave_smart)
4+
[![Python 3.7+](https://img.shields.io/badge/python-3.7+-blue.svg)](https://www.python.org/downloads/)
5+
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
56

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.
79

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...
918

10-
pip3 install lightwave_smart
19+
## Updates
1120

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.
1322

14-
## Using the library
23+
## Installation
1524

16-
### Imports
17-
You'll need to import the library
25+
```bash
26+
pip install lightwave_smart
27+
```
1828

19-
from lightwave_smart import lightwave_smart
29+
## Quick Start
2030

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
2234

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()
2540

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()
3043

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}")
4748

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)
4953

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()
5455

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+
```
5658

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
5860

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)
6063

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
6765

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)
6972

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)
7175

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+
```
8580

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+
```
8788

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}")
8995

90-
print(link.featuresets['5bc4d06e87779374d29d7d9a-5bc4d61387779374d29fdd1e'].features)
96+
# Register callback
97+
await link.async_register_feature_callback(featureset_id, feature_changed)
98+
```
9199

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
93101

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
95105

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.
98107

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
100113

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
105117

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
131119

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
172122

173-
See example_async.py for a minimal client.
123+
## Contributing
174124

175-
#### async methods
125+
Contributions welcome! Fork, create a feature branch, commit changes, and submit a PR.
176126

177-
The library is actually all built on async methods (the sync versions described above are just wrappers for the async versions)
127+
## License
178128

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.
188130

189131
## Thanks
190132

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

example_async.py

Lines changed: 25 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,33 @@
1-
from lightwave_smart import lightwave_smart
2-
import logging
31
import asyncio
2+
from lightwave_smart import lightwave_smart
3+
from logging import basicConfig, INFO
4+
from logging import getLogger
45

5-
USER = None
6-
PASSWORD = None
7-
8-
logging.basicConfig(level=logging.DEBUG)
9-
10-
user = USER if USER else input("Enter username: ")
11-
password = PASSWORD if PASSWORD else input("Enter password: ")
6+
basicConfig(level=INFO) # Set logging level to INFO
7+
_LOGGER = getLogger(__name__)
128

13-
#Define a callback function. The callback receives 4 pieces of information:
14-
#feature, feature_id, prev_value, new_value
15-
def alert(**kwargs):
16-
print("Callback received: {}".format(kwargs))
9+
def feature_changed(**kwargs):
10+
_LOGGER.info(f"Feature changed: {kwargs}")
1711

1812
async def main():
19-
#Following three lines are minimal requirement to initialise a connection
20-
#This will start up a background consumer_handler task that will run as long as the event loop is active
21-
#This will keep the states synchronised with the real world, and reconnect if the connection drops
22-
link = lightwave_smart.LWLink2(user, password)
23-
await link.async_connect()
13+
link = lightwave_smart.LWLink2()
14+
link.auth.set_auth_method(auth_method="password", username="your_email@example.com", password="your_password")
15+
await link.async_activate()
2416
await link.async_get_hierarchy()
25-
26-
#Following is optional, the background task will call the callback function where a change of state is detected
27-
await link.async_register_callback(alert)
28-
29-
#Dummy main program logic
30-
async def process_loop():
31-
n = 1
17+
18+
# Register callback for state changes
19+
# only register the last featureset for this example
20+
last_featureset = None
21+
for featureset in link.featuresets.values():
22+
last_featureset = featureset
23+
if last_featureset:
24+
_LOGGER.info(f"Registering callback for Feature Set: {last_featureset.name} (ID: {last_featureset.featureset_id}, Device ID: {last_featureset.device.device_id}, Product Code: {last_featureset.device.product_code})")
25+
await link.async_register_feature_callback(last_featureset.featureset_id, feature_changed)
26+
else:
27+
_LOGGER.warning("No featuresets found")
28+
29+
# Keep running to receive updates
3230
while True:
33-
print(f"{n}")
34-
await asyncio.sleep(2)
35-
n = n+1
36-
37-
loop = asyncio.get_event_loop()
38-
loop.run_until_complete(main())
39-
loop.run_until_complete(process_loop())
40-
31+
await asyncio.sleep(1)
4132

33+
asyncio.run(main())

0 commit comments

Comments
 (0)