Compare commits
17 Commits
1812642773
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 49e97e0a72 | |||
| 0be19a6211 | |||
| f730b9b8c6 | |||
| 54bdce2646 | |||
| 600194b16d | |||
| 071aa7a9ac | |||
| bdd6ab840c | |||
| b241bf10b0 | |||
| 8797b119f2 | |||
| 87ed481f80 | |||
| d4254be81b | |||
| a7ecb054c1 | |||
| 5dd7dd331b | |||
| 0d52d7dfcb | |||
| ee45fa097d | |||
| 2a1bb9ee63 | |||
| b2592a2f4c |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -2,4 +2,5 @@
|
||||
/*/
|
||||
!.gitignore
|
||||
!custom_components/
|
||||
**/__pycache__/
|
||||
**/__pycache__/
|
||||
!README.md
|
||||
|
||||
43
README.md
Normal file
43
README.md
Normal file
@@ -0,0 +1,43 @@
|
||||
Plejd integration for Home Assistant
|
||||
===
|
||||
|
||||
No Plejd hub needed.
|
||||
|
||||
This integration is a work in progress.
|
||||
|
||||
I do not guarantee it will work or even that it will not harm your system. I don't see what harm it *could* cause, but I promise nothing.
|
||||
|
||||
## Installation for testing
|
||||
|
||||
- Make sure you have a working Bluetooth integration in Home Assistant (a bluetooth proxy should work too)
|
||||
- Make sure you have no other plejd custom components or add-ons running.
|
||||
- Put the `plejd` directory in your `<config>/custom_components`.
|
||||
- Restart Home Assistant
|
||||
- Hopefully, your Plejd mesh will be auto discovered and you should see a message popping up in your integrations page.
|
||||
- Log in with the credentials you use in the Plejd app when prompted (email address and password)
|
||||
|
||||
|
||||
## Getting more debug information
|
||||
|
||||
Add this to your Home Assistant configuration.yaml to get as much information as possible
|
||||
|
||||
```yaml
|
||||
logger:
|
||||
default: warn
|
||||
logs:
|
||||
custom_components.plejd: debug
|
||||
```
|
||||
|
||||
## Other integrations
|
||||
|
||||
Here are some other Plejd integrations by some awesome people and how this is different:
|
||||
|
||||
I've pulled inspiration from all of them.
|
||||
|
||||
| | | |
|
||||
|---|---|---|
|
||||
|[hassio-plejd](https://github.com/icanos/hassio-plejd) | [@icanos](https://github.com/icanos) | Works only with Home Assistant OS.<br> Requires exclusive access to the Bluetooth dongle.<br> Does not support Bluetooth Proxy. |
|
||||
|[plejd2mqtt](https://github.com/thomasloven/plejd2mqtt) | [@thomasloven](https://github.com/thomasloven) | Somewhat outdated stand-alone version of the above.<br> Relies on MQTT for communication.<br> Requires exclusive access to the Bluetooth dongle.<br> Does not support Bluetooth Proxy.<br> Does not support switches or scenes. |
|
||||
|[ha-plejd](https://github.com/klali/ha-plejd) | [@klali](https://github.com/klali) <br> (also check [this fork](https://github.com/bnordli/ha-plejd/tree/to-integration) by [@bnordli](https://github.com/bnordli))| Does not communicate with the Plejd API and therefore requires you to extract the cryptokey and device data from the Plejd app somehow.<br> No auto discovery.<br> Requires exclusive access to the Bluetooth dongle.<br> Does not support Bluetooth Proxy. |
|
||||
| [homey-plejd](https://github.com/emilohman/homey-plejd) | [@emilohman](https://github.com/emilohman) | For Homey |
|
||||
| [homebridge-plejd](https://github.com/blommegard/homebridge-plejd) | [@blommegard](https://github.com/blommegard) | For Homebridge |
|
||||
@@ -15,7 +15,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DOMAIN = "plejd"
|
||||
|
||||
async def async_setup(hass, config):
|
||||
_LOGGER.error("Setting up plejd")
|
||||
if not hass.config_entries.async_entries("plejd"):
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
@@ -31,7 +30,9 @@ async def async_setup_entry(hass, config_entry):
|
||||
plejdManager = pyplejd.PlejdManager(config_entry.data)
|
||||
|
||||
devices = await plejdManager.get_devices()
|
||||
scenes = await plejdManager.get_scenes()
|
||||
|
||||
# Add a service entry if there are no devices - just so the user can get diagnostics data
|
||||
if sum(d.type in ["light", "switch"] for d in devices.values()) == 0:
|
||||
site_data = await plejdManager.get_site_data()
|
||||
|
||||
@@ -44,6 +45,22 @@ async def async_setup_entry(hass, config_entry):
|
||||
entry_type=dr.DeviceEntryType.SERVICE,
|
||||
)
|
||||
|
||||
hass.data.setdefault(DOMAIN, {}).update(
|
||||
{
|
||||
"stopping": False,
|
||||
}
|
||||
)
|
||||
hass.data[DOMAIN].setdefault("devices", {}).update({
|
||||
config_entry.entry_id: devices
|
||||
})
|
||||
hass.data[DOMAIN].setdefault("scenes", {}).update({
|
||||
config_entry.entry_id: scenes
|
||||
})
|
||||
hass.data[DOMAIN].setdefault("manager", {}).update({
|
||||
config_entry.entry_id: plejdManager,
|
||||
})
|
||||
|
||||
# Close any stale connections that may be open
|
||||
for dev in devices.values():
|
||||
ble_device = bluetooth.async_ble_device_from_address(
|
||||
hass, dev.BLE_address, True
|
||||
@@ -51,16 +68,30 @@ async def async_setup_entry(hass, config_entry):
|
||||
if ble_device:
|
||||
await plejdManager.close_stale(ble_device)
|
||||
|
||||
hass.data.setdefault(DOMAIN, {}).update(
|
||||
{
|
||||
"stopping": False,
|
||||
"manager": plejdManager,
|
||||
"devices": devices,
|
||||
}
|
||||
)
|
||||
# Search for devices in the mesh
|
||||
def _discovered_plejd(service_info, *_):
|
||||
plejdManager.add_mesh_device(service_info.device)
|
||||
bluetooth.async_register_callback(
|
||||
hass,
|
||||
_discovered_plejd,
|
||||
BluetoothCallbackMatcher(
|
||||
connectable=True,
|
||||
service_uuid=pyplejd.const.PLEJD_SERVICE.lower()
|
||||
),
|
||||
bluetooth.BluetoothScanningMode.PASSIVE
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, ["light", "switch"])
|
||||
# Run through already discovered devices and add plejds to the mesh
|
||||
for service_info in bluetooth.async_discovered_service_info(hass, True):
|
||||
if pyplejd.PLEJD_SERVICE.lower() in service_info.advertisement.service_uuids:
|
||||
plejdManager.add_mesh_device(service_info.device)
|
||||
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry,
|
||||
["light", "switch", "button"]
|
||||
)
|
||||
|
||||
# Ping mesh intermittently to keep the connection alive
|
||||
async def _ping(now=None):
|
||||
if hass.data[DOMAIN]["stopping"]: return
|
||||
if not await plejdManager.keepalive():
|
||||
@@ -72,17 +103,7 @@ async def async_setup_entry(hass, config_entry):
|
||||
)
|
||||
hass.async_create_task(_ping())
|
||||
|
||||
bluetooth.async_register_callback(
|
||||
hass,
|
||||
plejdManager.discover_plejd,
|
||||
BluetoothCallbackMatcher(
|
||||
connectable=True,
|
||||
service_uuid=pyplejd.const.PLEJD_SERVICE.lower()
|
||||
),
|
||||
bluetooth.BluetoothScanningMode.PASSIVE
|
||||
)
|
||||
|
||||
|
||||
# Cleanup when Home Assistant stops
|
||||
async def _stop(ev):
|
||||
hass.data[DOMAIN]["stopping"] = True
|
||||
if "ping_timer" in hass.data[DOMAIN]:
|
||||
@@ -91,13 +112,5 @@ async def async_setup_entry(hass, config_entry):
|
||||
config_entry.async_on_unload(
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop)
|
||||
)
|
||||
|
||||
for service_info in bluetooth.async_discovered_service_info(hass, True):
|
||||
if pyplejd.PLEJD_SERVICE.lower() in service_info.advertisement.service_uuids:
|
||||
plejdManager.discover_plejd(service_info)
|
||||
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
|
||||
47
custom_components/plejd/button.py
Normal file
47
custom_components/plejd/button.py
Normal file
@@ -0,0 +1,47 @@
|
||||
import logging
|
||||
|
||||
from homeassistant.components.button import ButtonEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "plejd"
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
scenes = hass.data[DOMAIN]["scenes"].get(config_entry.entry_id, [])
|
||||
|
||||
entities = []
|
||||
for s in scenes:
|
||||
button = PlejdSceneButton(s, config_entry.entry_id)
|
||||
entities.append(button)
|
||||
async_add_entities(entities, False)
|
||||
|
||||
class PlejdSceneButton(ButtonEntity):
|
||||
|
||||
def __init__(self, device, entry_id):
|
||||
super().__init__()
|
||||
self.device = device
|
||||
self.entry_id = entry_id
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
return {
|
||||
"identifiers": {(DOMAIN, f"{self.entry_id}:{self.device.index}")},
|
||||
"name": self.device.name,
|
||||
"manufacturer": "Plejd",
|
||||
#"connections": ???,
|
||||
}
|
||||
|
||||
@property
|
||||
def has_entity_name(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return None
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
return f"{self.entry_id}:{self.device.index}"
|
||||
|
||||
async def async_press(self):
|
||||
await self.device.activate()
|
||||
@@ -13,6 +13,8 @@ class PlejdConfigFlow(ConfigFlow, domain="plejd"):
|
||||
async def async_step_user(self, info=None):
|
||||
|
||||
if info is None:
|
||||
if self._async_current_entries():
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
@@ -22,31 +24,27 @@ class PlejdConfigFlow(ConfigFlow, domain="plejd"):
|
||||
}
|
||||
)
|
||||
)
|
||||
_LOGGER.info("Log in with %s %s", info["username"], info["password"])
|
||||
self.credentials = info
|
||||
return await self.async_step_picksite()
|
||||
|
||||
async def async_step_picksite(self, info=None):
|
||||
if info is None:
|
||||
sites = await api.get_sites(self.credentials["username"], self.credentials["password"])
|
||||
_LOGGER.info(sites)
|
||||
self.sites = {site["site"]["siteId"]: site["site"]["title"] for site in sites}
|
||||
return self.async_show_form(
|
||||
step_id="picksite",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required("site"): vol.In(
|
||||
{
|
||||
site["site"]["siteId"]: site["site"]["title"]
|
||||
for site in sites
|
||||
}
|
||||
)
|
||||
vol.Required("site"): vol.In(self.sites)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
siteTitle = self.sites[info["site"]]
|
||||
data={
|
||||
"username": self.credentials["username"],
|
||||
"password": self.credentials["password"],
|
||||
"siteId": info["site"]
|
||||
"siteId": info["site"],
|
||||
"siteTitle": siteTitle,
|
||||
}
|
||||
_LOGGER.debug("Saving: %s", data)
|
||||
return self.async_create_entry(title="Plejd", data=data)
|
||||
return self.async_create_entry(title=siteTitle, data=data)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from builtins import property
|
||||
import logging
|
||||
from homeassistant.components.light import LightEntity, ColorMode
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity, DataUpdateCoordinator
|
||||
@@ -9,16 +8,14 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DOMAIN = "plejd"
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
devices = hass.data[DOMAIN]["devices"]
|
||||
devices = hass.data[DOMAIN]["devices"].get(config_entry.entry_id, [])
|
||||
|
||||
entities = []
|
||||
for d in devices:
|
||||
dev = devices[d]
|
||||
if dev.type == "light":
|
||||
coordinator = Coordinator(hass, dev)
|
||||
async def updateCallback(data):
|
||||
coordinator.async_set_updated_data(data)
|
||||
dev.updateCallback = updateCallback
|
||||
dev.updateCallback = coordinator.async_set_updated_data
|
||||
light = PlejdLight(coordinator, dev)
|
||||
entities.append(light)
|
||||
async_add_entities(entities, False)
|
||||
@@ -38,14 +35,10 @@ class PlejdLight(LightEntity, CoordinatorEntity):
|
||||
def _data(self):
|
||||
return self.coordinator.data or {}
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
return self._data.get("state", None) is not None
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self.device.BLE_address)},
|
||||
"identifiers": {(DOMAIN, f"{self.device.BLE_address}:{self.device.address}")},
|
||||
"name": self.device.name,
|
||||
"manufacturer": "Plejd",
|
||||
"model": {self.device.model},
|
||||
@@ -64,15 +57,15 @@ class PlejdLight(LightEntity, CoordinatorEntity):
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
return self.device.BLE_address
|
||||
return f"{self.device.BLE_address}:{self.device.address}"
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
return self._data.get("state")
|
||||
return self.device.state
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
return self._data.get("dim",0)
|
||||
return self.device.dim
|
||||
|
||||
@property
|
||||
def supported_color_modes(self):
|
||||
|
||||
@@ -4,8 +4,8 @@ from datetime import timedelta
|
||||
from bleak_retry_connector import close_stale_connections
|
||||
|
||||
from .mesh import PlejdMesh
|
||||
from .api import get_cryptokey, get_devices, get_site_data
|
||||
from .plejd_device import PlejdDevice
|
||||
from .api import get_cryptokey, get_devices, get_site_data, get_scenes
|
||||
from .plejd_device import PlejdDevice, PlejdScene
|
||||
|
||||
from .const import PLEJD_SERVICE
|
||||
|
||||
@@ -18,14 +18,19 @@ class PlejdManager:
|
||||
self.mesh = PlejdMesh()
|
||||
self.mesh.statecallback = self._update_device
|
||||
self.devices = { }
|
||||
self.scenes = []
|
||||
self.credentials = credentials
|
||||
|
||||
def discover_plejd(self, service_info, *_):
|
||||
_LOGGER.debug("Adding plejd %s", service_info)
|
||||
self.mesh.add_mesh_node(service_info.device)
|
||||
def add_mesh_device(self, device):
|
||||
_LOGGER.debug("Adding plejd %s", device)
|
||||
# for d in self.devices.values():
|
||||
# addr = device.address.replace(":","").replace("-","").upper()
|
||||
# if d.BLE_address.upper() == addr or addr in device.name:
|
||||
return self.mesh.add_mesh_node(device)
|
||||
# _LOGGER.debug("Device was not expected in current mesh")
|
||||
|
||||
async def close_stale(self, device):
|
||||
_LOGGER.info("Closing stale connections for %s", device)
|
||||
_LOGGER.debug("Closing stale connections for %s", device)
|
||||
await close_stale_connections(device)
|
||||
|
||||
@property
|
||||
@@ -38,10 +43,17 @@ class PlejdManager:
|
||||
async def get_devices(self):
|
||||
devices = await get_devices(**self.credentials)
|
||||
self.devices = {k: PlejdDevice(self, **v) for (k,v) in devices.items()}
|
||||
_LOGGER.info("Devices")
|
||||
_LOGGER.info(self.devices)
|
||||
_LOGGER.debug("Devices")
|
||||
_LOGGER.debug(self.devices)
|
||||
return self.devices
|
||||
|
||||
async def get_scenes(self):
|
||||
scenes = await get_scenes(**self.credentials)
|
||||
self.scenes = [PlejdScene(self, **s) for s in scenes]
|
||||
_LOGGER.debug("Scenes")
|
||||
_LOGGER.debug(self.scenes)
|
||||
return self.scenes
|
||||
|
||||
async def _update_device(self, deviceState):
|
||||
address = deviceState["address"]
|
||||
if address in self.devices:
|
||||
@@ -60,7 +72,10 @@ class PlejdManager:
|
||||
if not self.mesh.connected:
|
||||
if not await self.mesh.connect():
|
||||
return False
|
||||
return await self.mesh.ping()
|
||||
retval = await self.mesh.ping()
|
||||
if retval and self.mesh.pollonWrite:
|
||||
await self.mesh.poll()
|
||||
return retval
|
||||
|
||||
async def disconnect(self):
|
||||
_LOGGER.debug("DISCONNECT")
|
||||
|
||||
@@ -43,26 +43,23 @@ async def _get_site_details(session, siteId):
|
||||
# fp.write(json.dumps(data))
|
||||
return data
|
||||
|
||||
site_data = None
|
||||
async def get_site_data(username, password, siteId):
|
||||
site_data = {}
|
||||
async def get_site_data(username, password, siteId, **_):
|
||||
global site_data
|
||||
if site_data is not None:
|
||||
return site_data
|
||||
if site_data.get(siteId) is not None:
|
||||
return site_data.get(siteId)
|
||||
async with ClientSession(base_url=API_BASE_URL, headers=headers) as session:
|
||||
session_token = await _login(session, username, password)
|
||||
_LOGGER.debug("Session token: %s", session_token)
|
||||
session.headers["X-Parse-Session-Token"] = session_token
|
||||
details = await _get_site_details(session, siteId)
|
||||
site_data = details
|
||||
site_data[siteId] = details
|
||||
return details
|
||||
|
||||
async def get_sites(username, password):
|
||||
async def get_sites(username, password, **_):
|
||||
async with ClientSession(base_url=API_BASE_URL, headers=headers) as session:
|
||||
session_token = await _login(session, username, password)
|
||||
_LOGGER.debug("Session token: %s", session_token)
|
||||
session.headers["X-Parse-Session-Token"] = session_token
|
||||
sites = await _get_sites(session)
|
||||
_LOGGER.debug("Sites: %s", sites)
|
||||
return sites["result"]
|
||||
|
||||
|
||||
@@ -77,13 +74,11 @@ async def get_devices(**credentials):
|
||||
for device in site_data["devices"]:
|
||||
BLE_address = device["deviceId"]
|
||||
|
||||
def find_deviceId(d):
|
||||
return next((s for s in d if s["deviceId"] == BLE_address), None)
|
||||
|
||||
address = site_data["deviceAddress"][BLE_address]
|
||||
dimmable = None
|
||||
|
||||
settings = find_deviceId(site_data["outputSettings"])
|
||||
settings = next((s for s in site_data["outputSettings"]
|
||||
if s["deviceParseId"] == device["objectId"]))
|
||||
if settings is not None:
|
||||
outputs = site_data["outputAddress"][BLE_address]
|
||||
address = outputs[str(settings["output"])]
|
||||
@@ -91,9 +86,10 @@ async def get_devices(**credentials):
|
||||
if settings is not None and settings["dimCurve"] == "nonDimmable":
|
||||
dimmable = False
|
||||
|
||||
plejdDevice = find_deviceId(site_data["plejdDevices"])
|
||||
plejdDevice = next((s for s in site_data["plejdDevices"]
|
||||
if s["deviceId"] == BLE_address))
|
||||
room = next((r for r in site_data["rooms"] if r["roomId"] == device["roomId"]), {})
|
||||
|
||||
|
||||
retval[address] = {
|
||||
"address": address,
|
||||
"BLE_address": BLE_address,
|
||||
@@ -107,3 +103,18 @@ async def get_devices(**credentials):
|
||||
}
|
||||
|
||||
return retval
|
||||
|
||||
async def get_scenes(**credentials):
|
||||
site_data = await get_site_data(**credentials)
|
||||
retval = []
|
||||
for scene in site_data["scenes"]:
|
||||
if scene["hiddenFromSceneList"]: continue
|
||||
sceneId = scene["sceneId"]
|
||||
index = site_data["sceneIndex"].get(sceneId)
|
||||
|
||||
retval.append({
|
||||
"index": index,
|
||||
"title": scene["title"],
|
||||
})
|
||||
|
||||
return retval
|
||||
|
||||
@@ -3,6 +3,7 @@ import binascii
|
||||
import logging
|
||||
import os
|
||||
import struct
|
||||
from datetime import datetime
|
||||
from bleak import BleakClient, BleakError
|
||||
from bleak_retry_connector import establish_connection
|
||||
|
||||
@@ -24,7 +25,10 @@ class PlejdMesh():
|
||||
self.statecallback = None
|
||||
|
||||
def add_mesh_node(self, device):
|
||||
self.mesh_nodes.append(device)
|
||||
if device not in self.mesh_nodes:
|
||||
self.mesh_nodes.append(device)
|
||||
else:
|
||||
_LOGGER.debug("Plejd already added")
|
||||
|
||||
def set_crypto_key(self, key):
|
||||
self.crypto_key = binascii.a2b_hex(key.replace("-", ""))
|
||||
@@ -48,13 +52,13 @@ class PlejdMesh():
|
||||
|
||||
async def connect(self, disconnect_callback=None, key=None):
|
||||
await self.disconnect()
|
||||
_LOGGER.info("Trying to connect")
|
||||
_LOGGER.debug("Trying to connect to mesh")
|
||||
|
||||
client = None
|
||||
|
||||
def _disconnect(arg):
|
||||
if not self.connected: return
|
||||
_LOGGER.error("_disconnect %s", arg)
|
||||
_LOGGER.debug("_disconnect %s", arg)
|
||||
self.client = None
|
||||
self._connected = False
|
||||
if disconnect_callback:
|
||||
@@ -63,42 +67,50 @@ class PlejdMesh():
|
||||
self.mesh_nodes.sort(key = lambda a: a.rssi, reverse = True)
|
||||
for plejd in self.mesh_nodes:
|
||||
try:
|
||||
_LOGGER.warning("Connecting to %s", plejd)
|
||||
_LOGGER.debug("Connecting to %s", plejd)
|
||||
client = await establish_connection(BleakClient, plejd, "plejd", _disconnect)
|
||||
address = plejd.address
|
||||
self._connected = True
|
||||
self.client = client
|
||||
_LOGGER.debug("Connected to Plejd mesh")
|
||||
await asyncio.sleep(2)
|
||||
if not await self._authenticate():
|
||||
await self.client.disconnect()
|
||||
self._connected = False
|
||||
continue
|
||||
break
|
||||
except (BleakError, asyncio.TimeoutError) as e:
|
||||
_LOGGER.error("Error connecting to plejd: %s", str(e))
|
||||
_LOGGER.warning("Error connecting to Plejd device: %s", str(e))
|
||||
else:
|
||||
if len(self.mesh_nodes) == 0:
|
||||
_LOGGER.debug("Failed to connect to plejd mesh - no devices discovered")
|
||||
else:
|
||||
_LOGGER.warning("Failed to connect to plejd mesh - %s", self.mesh_nodes)
|
||||
return False
|
||||
|
||||
self.client = client
|
||||
self.connected_node = binascii.a2b_hex(address.replace(":", "").replace("-", ""))[::-1]
|
||||
|
||||
await asyncio.sleep(2)
|
||||
|
||||
if not await self._authenticate():
|
||||
await self.disconnect()
|
||||
return False
|
||||
|
||||
async def _lastdata(_, lastdata):
|
||||
self.pollonWrite = False
|
||||
data = encrypt_decrypt(self.crypto_key, self.connected_node, lastdata)
|
||||
_LOGGER.debug("Received LastData %s", data)
|
||||
deviceState = decode_state(data)
|
||||
_LOGGER.debug("Lastdata %s", deviceState)
|
||||
_LOGGER.debug("Decoded LastData %s", deviceState)
|
||||
if self.statecallback and deviceState is not None:
|
||||
await self.statecallback(deviceState)
|
||||
|
||||
async def _lightlevel(_, lightlevel):
|
||||
deviceState = {
|
||||
"address": int(lightlevel[0]),
|
||||
"state": bool(lightlevel[1]),
|
||||
"dim": int.from_bytes(lightlevel[5:7], "little"),
|
||||
}
|
||||
_LOGGER.debug("Lightlevel %s", deviceState)
|
||||
if self.statecallback and deviceState is not None:
|
||||
await self.statecallback(deviceState)
|
||||
_LOGGER.debug("Received LightLevel %s", lightlevel)
|
||||
for i in range(0, len(lightlevel), 10):
|
||||
ll = lightlevel[i:i+10]
|
||||
deviceState = {
|
||||
"address": int(ll[0]),
|
||||
"state": bool(ll[1]),
|
||||
"dim": int.from_bytes(ll[5:7], "little"),
|
||||
}
|
||||
_LOGGER.debug("Decoded LightLevel %s", deviceState)
|
||||
if self.statecallback and deviceState is not None:
|
||||
await self.statecallback(deviceState)
|
||||
|
||||
await client.start_notify(PLEJD_LASTDATA, _lastdata)
|
||||
await client.start_notify(PLEJD_LIGHTLEVEL, _lightlevel)
|
||||
@@ -109,10 +121,11 @@ class PlejdMesh():
|
||||
|
||||
async def write(self, payload):
|
||||
try:
|
||||
_LOGGER.debug("Writing data to Plejd mesh CT: %s", payload)
|
||||
data = encrypt_decrypt(self.crypto_key, self.connected_node, payload)
|
||||
await self.client.write_gatt_char(PLEJD_DATA, data, response=True)
|
||||
except (BleakError, asyncio.TimeoutError) as e:
|
||||
_LOGGER.error("Write failed: %s", str(e))
|
||||
_LOGGER.warning("Plejd mesh write command failed: %s", str(e))
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -123,6 +136,13 @@ class PlejdMesh():
|
||||
await self.poll()
|
||||
return retval
|
||||
|
||||
async def activate_scene(self, index):
|
||||
payload = binascii.a2b_hex(f"0201100021{index:02x}")
|
||||
retval = await self.write(payload)
|
||||
if self.pollonWrite:
|
||||
await self.poll()
|
||||
return retval
|
||||
|
||||
async def ping(self):
|
||||
if self.client is None:
|
||||
return False
|
||||
@@ -135,25 +155,29 @@ class PlejdMesh():
|
||||
if (ping[0] + 1) & 0xFF == pong[0]:
|
||||
return True
|
||||
except (BleakError, asyncio.TimeoutError) as e:
|
||||
_LOGGER.warning("Error sending ping: %s", str(e))
|
||||
_LOGGER.warning("Plejd mesh keepalive signal failed: %s", str(e))
|
||||
self.pollonWrite = True
|
||||
return False
|
||||
|
||||
async def poll(self):
|
||||
if self.client is None:
|
||||
return
|
||||
_LOGGER.debug("Polling Plejd mesh for current state")
|
||||
await self.client.write_gatt_char(PLEJD_LIGHTLEVEL, b"\x01", response=True)
|
||||
|
||||
async def _authenticate(self):
|
||||
if self.client is None:
|
||||
return False
|
||||
try:
|
||||
_LOGGER.debug("Authenticating")
|
||||
_LOGGER.debug("Authenticating to plejd mesh")
|
||||
await self.client.write_gatt_char(PLEJD_AUTH, b"\0x00", response=True)
|
||||
challenge = await self.client.read_gatt_char(PLEJD_AUTH)
|
||||
response = auth_response(self.crypto_key, challenge)
|
||||
await self.client.write_gatt_char(PLEJD_AUTH, response, response=True)
|
||||
_LOGGER.debug("Authenticated")
|
||||
if not await self.ping():
|
||||
_LOGGER.debug("Authenticion failed")
|
||||
return False
|
||||
_LOGGER.debug("Authenticated successfully")
|
||||
return True
|
||||
except (BleakError, asyncio.TimeoutError) as e:
|
||||
_LOGGER.warning("Plejd authentication failed: %s", str(e))
|
||||
@@ -166,7 +190,8 @@ def decode_state(data):
|
||||
if address == 1 and cmd == b"\x00\x1b":
|
||||
_LOGGER.debug("Got time data?")
|
||||
ts = struct.unpack_from("<I", data, 5)[0]
|
||||
_LOGGER.debug("Timestamp: %s", ts)
|
||||
dt = datetime.fromtimestamp(ts)
|
||||
_LOGGER.debug("Timestamp: %s (%s)", ts, dt)
|
||||
return None
|
||||
|
||||
dim, state = None, None
|
||||
@@ -175,6 +200,12 @@ def decode_state(data):
|
||||
dim = int.from_bytes(data[6:8], "little")
|
||||
elif cmd == b"\x00\x97":
|
||||
state = bool(data[5])
|
||||
elif cmd == b"\x00\x16":
|
||||
_LOGGER.debug("A button was pressed")
|
||||
return None
|
||||
else:
|
||||
_LOGGER.debug("Unknown command %s", cmd)
|
||||
return None
|
||||
|
||||
return {
|
||||
"address": address,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
|
||||
from builtins import property
|
||||
from collections import namedtuple
|
||||
import logging
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
Device = namedtuple("Device", ["model", "type", "dimmable"])
|
||||
|
||||
@@ -20,10 +23,10 @@ HARDWARE_TYPES = {
|
||||
"7": Device("REL-01", SWITCH, False),
|
||||
"8": Device("SPR-01", SWITCH, False),
|
||||
"10": Device("WRT-01", SWITCH, False),
|
||||
"11": Device("DIM-01", LIGHT, True),
|
||||
"11": Device("DIM-01-2P", LIGHT, True),
|
||||
"13": Device("Generic", LIGHT, False),
|
||||
"14": Device("DIM-01", LIGHT, True),
|
||||
"15": Device("DIM-02", LIGHT, True),
|
||||
"14": Device("DIM-01-LC", LIGHT, True),
|
||||
"15": Device("DIM-02-LC", LIGHT, True),
|
||||
"17": Device("REL-01-2P", SWITCH, False),
|
||||
"18": Device("REL-02", SWITCH, False),
|
||||
"20": Device("SPR-01", SWITCH, False),
|
||||
@@ -43,13 +46,20 @@ class PlejdDevice:
|
||||
self._state = None
|
||||
self._dim = None
|
||||
|
||||
def __repr__(self):
|
||||
return f"<PlejdDevice(<manager>, {self.address}, {self.BLE_address}, {self.data}>"
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
return self._state is not None
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
return self._state
|
||||
return self._state if self.available else False
|
||||
|
||||
@property
|
||||
def dim(self):
|
||||
return self._dim/255
|
||||
return self._dim/255 if self._dim else 0
|
||||
|
||||
@property
|
||||
def BLE_address(self):
|
||||
@@ -93,7 +103,7 @@ class PlejdDevice:
|
||||
self._dim = dim
|
||||
if update:
|
||||
if self.updateCallback:
|
||||
await self.updateCallback({"state": self._state, "dim": self._dim})
|
||||
self.updateCallback({"state": self._state, "dim": self._dim})
|
||||
|
||||
async def turn_on(self, dim=0):
|
||||
await self.manager.mesh.set_state(self.address, True, dim)
|
||||
@@ -101,4 +111,23 @@ class PlejdDevice:
|
||||
async def turn_off(self):
|
||||
await self.manager.mesh.set_state(self.address, False)
|
||||
|
||||
|
||||
class PlejdScene:
|
||||
|
||||
def __init__(self, manager, index, title):
|
||||
self._manager = manager
|
||||
self._index = index
|
||||
self._title = title
|
||||
|
||||
def __repr__(self):
|
||||
return f"<PlejdScene(<manager>, {self._index}, '{self._title}'>"
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._title
|
||||
|
||||
@property
|
||||
def index(self):
|
||||
return self._index
|
||||
|
||||
async def activate(self):
|
||||
await self._manager.mesh.activate_scene(self._index)
|
||||
|
||||
@@ -9,16 +9,14 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DOMAIN = "plejd"
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
devices = hass.data[DOMAIN]["devices"]
|
||||
devices = hass.data[DOMAIN]["devices"].get(config_entry.entry_id, [])
|
||||
|
||||
entities = []
|
||||
for d in devices:
|
||||
dev = devices[d]
|
||||
if dev.type == "switch":
|
||||
coordinator = Coordinator(hass, dev)
|
||||
async def updateCallback(data):
|
||||
coordinator.async_set_updated_data(data)
|
||||
dev.updateCallback = updateCallback
|
||||
dev.updateCallback = coordinator.async_set_updated_data
|
||||
switch = PlejdSwitch(coordinator, dev)
|
||||
entities.append(switch)
|
||||
async_add_entities(entities, False)
|
||||
@@ -38,14 +36,10 @@ class PlejdSwitch(SwitchEntity, CoordinatorEntity):
|
||||
def _data(self):
|
||||
return self.coordinator.data or {}
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
return self._data.get("state", None) is not None
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self.device.BLE_address)},
|
||||
"identifiers": {(DOMAIN, f"{self.device.BLE_address}:{self.device.address}")},
|
||||
"name": self.device.name,
|
||||
"manufacturer": "Plejd",
|
||||
"model": self.device.model,
|
||||
@@ -64,7 +58,7 @@ class PlejdSwitch(SwitchEntity, CoordinatorEntity):
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
return self.device.BLE_address
|
||||
return f"{self.device.BLE_address}:{self.device.address}"
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"title": "Plejd",
|
||||
"config": {
|
||||
"abort": {
|
||||
"single_instance_allowed": "Only one Plejd instance at a time is currently supported."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Log in to Plejd",
|
||||
|
||||
Reference in New Issue
Block a user