Compare commits

..

17 Commits

12 changed files with 298 additions and 120 deletions

3
.gitignore vendored
View File

@@ -2,4 +2,5 @@
/*/
!.gitignore
!custom_components/
**/__pycache__/
**/__pycache__/
!README.md

43
README.md Normal file
View 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 |

View File

@@ -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

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

View File

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

View File

@@ -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):

View File

@@ -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")

View File

@@ -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

View File

@@ -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,

View File

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

View File

@@ -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):

View File

@@ -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",