├── .gitmodules ├── hacs.json ├── custom_components └── plejd │ ├── const.py │ ├── manifest.json │ ├── translations │ └── en.json │ ├── scene.py │ ├── switch.py │ ├── binary_sensor.py │ ├── diagnostics.py │ ├── __init__.py │ ├── cover.py │ ├── plejd_entity.py │ ├── light.py │ ├── event.py │ ├── config_flow.py │ └── plejd_site.py ├── .gitignore ├── .github └── workflows │ ├── hassfest.yml │ └── hacs_validate.yml ├── .vscode ├── tasks.json └── launch.json ├── .devcontainer └── devcontainer.json ├── parse_json.py └── README.md /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "pyplejd"] 2 | path = pyplejd 3 | url = https://github.com/thomasloven/pyplejd 4 | -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Plejd", 3 | "homeassistant": "2024.02.0", 4 | "render_readme": true 5 | } 6 | -------------------------------------------------------------------------------- /custom_components/plejd/const.py: -------------------------------------------------------------------------------- 1 | """Constants for the Plejd component.""" 2 | 3 | DOMAIN = "plejd" 4 | MANUFACTURER = "Plejd" 5 | 6 | CONF_SITE_ID = "siteId" 7 | CONF_SITE_TITLE = "siteTitle" 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /* 2 | /*/ 3 | !.gitignore 4 | !.gitmodules 5 | !hacs.json 6 | !custom_components/ 7 | custom_components/*/ 8 | !custom_components/plejd 9 | **/__pycache__/ 10 | !README.md 11 | !pyplejd 12 | !.devcontainer/ 13 | !.vscode/ 14 | !.github/ 15 | !parse_json.py -------------------------------------------------------------------------------- /.github/workflows/hassfest.yml: -------------------------------------------------------------------------------- 1 | name: Validate with hassfest 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 0 * * *" 8 | 9 | jobs: 10 | validate: 11 | runs-on: "ubuntu-latest" 12 | steps: 13 | - uses: "actions/checkout@v4" 14 | - uses: "home-assistant/actions/hassfest@master" 15 | -------------------------------------------------------------------------------- /.github/workflows/hacs_validate.yml: -------------------------------------------------------------------------------- 1 | name: HACS Action 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 0 * * *" 8 | 9 | jobs: 10 | hacs: 11 | name: HACS Action 12 | runs-on: "ubuntu-latest" 13 | steps: 14 | - name: HACS Action 15 | uses: "hacs/action@main" 16 | with: 17 | category: "integration" 18 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Run hass", 6 | "type": "shell", 7 | "command": "sudo hass -v -c /config --skip-pip-packages pyplejd", 8 | "problemMatcher": [], 9 | "presentation": { 10 | "panel": "shared", 11 | "group": "test" 12 | } 13 | }, 14 | { 15 | "label": "Run hassfest", 16 | "type": "shell", 17 | "command": "hassfest", 18 | "problemMatcher": [] 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /custom_components/plejd/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "plejd", 3 | "name": "Plejd BLE", 4 | "bluetooth": [ 5 | { 6 | "service_uuid": "31ba0001-6085-4726-be45-040c957391b5", 7 | "connectable": true 8 | } 9 | ], 10 | "codeowners": ["@thomasloven"], 11 | "config_flow": true, 12 | "dependencies": ["bluetooth_adapters"], 13 | "documentation": "https://github.com/thomasloven/hass_plejd", 14 | "integration_type": "hub", 15 | "iot_class": "local_push", 16 | "issue_tracker": "https://github.com/thomasloven/hass_plejd/issues", 17 | "loggers": ["pyplejd"], 18 | "requirements": ["pyplejd==0.14.7"], 19 | "version": "0.14.7" 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: Remote Attach", 9 | "type": "python", 10 | "request": "attach", 11 | "connect": { 12 | "host": "localhost", 13 | "port": 5678 14 | }, 15 | "pathMappings": [ 16 | { 17 | "localRoot": "${workspaceFolder}", 18 | "remoteRoot": "/config" 19 | } 20 | ], 21 | "justMyCode": false 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /custom_components/plejd/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Plejd", 3 | "config": { 4 | "abort": { 5 | "single_discovery_only": "Only one configuration can be configured through discovery.", 6 | "no_device_discovered": "No plejd bluetooth mesh was found.", 7 | "bluetooth_not_available": "At least one Bluetooth adapter or remote must be configured to use Plejd.", 8 | "already_configured": "The chosen site has already been configured.", 9 | "reauth_successful": "Reauthorization was successful.", 10 | "no_cloud_connection": "Connection to the Plejd cloud API failed. Please try again later." 11 | }, 12 | "step": { 13 | "user": { 14 | "title": "Log in to Plejd", 15 | "data": { 16 | "username": "Email address", 17 | "password": "Password" 18 | } 19 | }, 20 | "picksite": { 21 | "title": "Pick your site" 22 | }, 23 | "reauth_confirm": { 24 | "title": "Reauthentication required", 25 | "description": "Your Plejd cloud username and password needs to be verified." 26 | } 27 | }, 28 | "error": { 29 | "faulty_credentials": "Wrong username or password." 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hass-plejd Dev", 3 | "image": "thomasloven/hass-custom-devcontainer", 4 | "postCreateCommand": "sudo -E container setup-dev", 5 | "containerEnv": { 6 | "DEVCONTAINER": "1", 7 | "TZ": "Europe/Stockholm" 8 | }, 9 | "forwardPorts": [8123], 10 | "mounts": [ 11 | "source=${localWorkspaceFolder}/custom_components,target=/config/custom_components,type=bind", 12 | "source=${localWorkspaceFolder}/pyplejd/pyplejd,target=/config/pyplejd,type=bind", 13 | "source=${localWorkspaceFolder}/pyplejd/pyplejd,target=/usr/local/bin/pyplejd,type=bind" 14 | ], 15 | "customizations": { 16 | "vscode": { 17 | "extensions": [ 18 | "esbenp.prettier-vscode", 19 | "ms-python.python", 20 | "ms-python.black-formatter" 21 | ], 22 | "settings": { 23 | "files.eol": "\n", 24 | "editor.tabSize": 2, 25 | "editor.formatOnPaste": false, 26 | "editor.formatOnSave": true, 27 | "editor.formatOnType": true, 28 | "files.trimTrailingWhitespace": true, 29 | "python.linting.pylintEnabled": false, 30 | "python.linting.enabled": true, 31 | "python.formatting.provider": "black" 32 | } 33 | } 34 | }, 35 | "runArgs": ["--network=host"] 36 | } 37 | -------------------------------------------------------------------------------- /parse_json.py: -------------------------------------------------------------------------------- 1 | from pyplejd.cloud import PlejdCloudSite, site_details as sd 2 | from pyplejd.interface import outputDeviceClass, inputDeviceClass, sceneDeviceClass 3 | import json 4 | import sys 5 | 6 | fn = "site_details_wms01.json" 7 | fn = "site_details_wms01.json" 8 | fn = "site_details_gwy.json" 9 | 10 | 11 | def main(filename): 12 | site = PlejdCloudSite("", "", "") 13 | with open(filename, "r") as fp: 14 | details = json.load(fp) 15 | if "data" in details: 16 | details = details["data"] 17 | site._details_raw = details 18 | site.details = sd.SiteDetails(**site._details_raw) 19 | 20 | print("Output Devices:") 21 | for output in site.outputs: 22 | cls = outputDeviceClass(output) 23 | # pprint.pp(output) 24 | print(cls(mesh=None, **output)) 25 | # print("") 26 | 27 | print("Input Devices:") 28 | for input in site.inputs: 29 | cls = inputDeviceClass(input) 30 | # pprint.pp(input) 31 | print(cls(mesh=None, **input)) 32 | # print("") 33 | 34 | print("Scenes:") 35 | for scene in site.scenes: 36 | cls = sceneDeviceClass(scene) 37 | # pprint.pp(input) 38 | print(cls(mesh=None, **scene)) 39 | 40 | 41 | def usage(): 42 | print(f"usage: {sys.argv[0]} ") 43 | print(" Filename is the diagnostics data file (.json)") 44 | 45 | 46 | if __name__ == "__main__": 47 | if not len(sys.argv) == 2: 48 | usage() 49 | sys.exit() 50 | filename = sys.argv[1] 51 | main(filename) 52 | -------------------------------------------------------------------------------- /custom_components/plejd/scene.py: -------------------------------------------------------------------------------- 1 | from homeassistant.components.scene import Scene 2 | from homeassistant.config_entries import ConfigEntry 3 | from homeassistant.core import callback, HomeAssistant 4 | 5 | from .plejd_site import dt, get_plejd_site_from_config_entry 6 | from .plejd_entity import PlejdDeviceBaseEntity 7 | 8 | 9 | async def async_setup_entry( 10 | hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities 11 | ) -> None: 12 | """Set up the Plejd scenes from a config entry.""" 13 | site = get_plejd_site_from_config_entry(hass, config_entry) 14 | 15 | @callback 16 | def async_add_scene(scene: dt.PlejdScene) -> None: 17 | """Add light from Plejd.""" 18 | if scene.hidden: 19 | return 20 | entity = PlejdSceneEntity(scene) 21 | async_add_entities([entity]) 22 | 23 | site.register_platform_add_device_callback( 24 | async_add_scene, dt.PlejdDeviceType.SCENE 25 | ) 26 | 27 | 28 | class PlejdSceneEntity(PlejdDeviceBaseEntity, Scene): 29 | """Representation of a Plejd scene.""" 30 | 31 | _attr_has_entity_name = True 32 | device_info = None 33 | 34 | def __init__(self, scene: dt.PlejdScene) -> None: 35 | """Set up scene.""" 36 | super().__init__(scene) 37 | self.device: dt.PlejdScene 38 | 39 | @property 40 | def name(self) -> str: 41 | """Return the name of the scene entity.""" 42 | return self.device.name 43 | 44 | async def async_activate(self, **_) -> None: 45 | """Activate the scene""" 46 | await self.device.activate() 47 | -------------------------------------------------------------------------------- /custom_components/plejd/switch.py: -------------------------------------------------------------------------------- 1 | """Support for Plejd switches.""" 2 | 3 | from homeassistant.components.switch import SwitchEntity 4 | from homeassistant.config_entries import ConfigEntry 5 | from homeassistant.core import callback, HomeAssistant 6 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 7 | 8 | from .plejd_site import dt, get_plejd_site_from_config_entry 9 | from .plejd_entity import PlejdDeviceBaseEntity 10 | 11 | 12 | async def async_setup_entry( 13 | hass: HomeAssistant, 14 | config_entry: ConfigEntry, 15 | async_add_entities: AddEntitiesCallback, 16 | ) -> None: 17 | """Set up the Plejd switches from a config entry.""" 18 | site = get_plejd_site_from_config_entry(hass, config_entry) 19 | 20 | @callback 21 | def async_add_switch(device: dt.PlejdRelay) -> None: 22 | """Add light from Plejd.""" 23 | entity = PlejdSwitch(device) 24 | async_add_entities([entity]) 25 | 26 | site.register_platform_add_device_callback( 27 | async_add_switch, dt.PlejdDeviceType.SWITCH 28 | ) 29 | 30 | 31 | class PlejdSwitch(PlejdDeviceBaseEntity, SwitchEntity): 32 | """Representation of a Plejd switch.""" 33 | 34 | def __init__(self, device: dt.PlejdRelay) -> None: 35 | """Set up switch.""" 36 | SwitchEntity.__init__(self) 37 | PlejdDeviceBaseEntity.__init__(self, device) 38 | self.device: dt.PlejdRelay 39 | 40 | @property 41 | def is_on(self) -> bool: 42 | """Returns true if switch is on.""" 43 | return self._data.get("state", False) 44 | 45 | async def async_turn_on(self, **_) -> None: 46 | """Turn the switch on.""" 47 | await self.device.turn_on() 48 | 49 | async def async_turn_off(self, **_) -> None: 50 | """Turn the switch off.""" 51 | await self.device.turn_off() 52 | -------------------------------------------------------------------------------- /custom_components/plejd/binary_sensor.py: -------------------------------------------------------------------------------- 1 | """Support for Plejd binary sensors.""" 2 | 3 | from homeassistant.components.binary_sensor import ( 4 | BinarySensorEntity, 5 | BinarySensorDeviceClass, 6 | ) 7 | from homeassistant.config_entries import ConfigEntry 8 | from homeassistant.core import callback, HomeAssistant 9 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 10 | 11 | from .plejd_site import ( 12 | dt, 13 | get_plejd_site_from_config_entry, 14 | ) 15 | from .plejd_entity import PlejdDeviceBaseEntity 16 | 17 | 18 | async def async_setup_entry( 19 | hass: HomeAssistant, 20 | config_entry: ConfigEntry, 21 | async_add_entities: AddEntitiesCallback, 22 | ): 23 | """Set up the Plejd events from a config entry.""" 24 | site = get_plejd_site_from_config_entry(hass, config_entry) 25 | 26 | @callback 27 | def async_add_motion_sensor(device: PlejdMotionSensor): 28 | """Add motion sensor from Plejd.""" 29 | entity = PlejdMotionSensor(device, hass) 30 | async_add_entities([entity]) 31 | 32 | site.register_platform_add_device_callback( 33 | async_add_motion_sensor, dt.PlejdDeviceType.MOTION 34 | ) 35 | 36 | 37 | class PlejdMotionSensor(PlejdDeviceBaseEntity, BinarySensorEntity): 38 | """Motion sensors in Plejd.""" 39 | 40 | _attr_device_class = BinarySensorDeviceClass.MOTION 41 | 42 | def __init__(self, device: dt.PlejdMotionSensor, hass) -> None: 43 | """Set up motion sensor.""" 44 | BinarySensorEntity.__init__(self) 45 | PlejdDeviceBaseEntity.__init__(self, device) 46 | self.device: dt.PlejdMotionSensor 47 | 48 | self.is_on = False 49 | 50 | @callback 51 | def _handle_update(self, state) -> None: 52 | """When motion is detected from Plejd.""" 53 | if state.get("motion", False) is not None: 54 | self.is_on = state.get("motion", False) 55 | -------------------------------------------------------------------------------- /custom_components/plejd/diagnostics.py: -------------------------------------------------------------------------------- 1 | """Diagnostic support for Plejd.""" 2 | 3 | from typing import TypeVar 4 | from pyplejd import PlejdManager 5 | from homeassistant.config_entries import ConfigEntry 6 | from homeassistant.core import HomeAssistant 7 | 8 | from .plejd_site import get_plejd_site_from_config_entry, PlejdSite 9 | 10 | 11 | REDACT_KEYS = { 12 | "site": { 13 | "previousOwners": True, 14 | "siteId": True, 15 | "astroTable": True, 16 | "city": True, 17 | "coordinates": True, 18 | "country": True, 19 | "deviceAstroTable": True, 20 | "zipCode": True, 21 | }, 22 | "plejdMesh": { 23 | "siteId": True, 24 | "plejdMeshId": True, 25 | "meshKey": True, 26 | "cryptoKey": True, 27 | }, 28 | "rooms": { 29 | "siteId": True, 30 | }, 31 | "scenes": { 32 | "siteId": True, 33 | }, 34 | "devices": { 35 | "siteId": True, 36 | }, 37 | "plejdDevices": { 38 | "siteId": True, 39 | "installer": True, 40 | "coordinates": True, 41 | }, 42 | "gateways": True, 43 | "resourceSets": True, 44 | "timeEvents": True, 45 | "sceneSteps": True, 46 | "astroEvents": True, 47 | "inputSettings": { 48 | "siteId": True, 49 | }, 50 | "outputSettings": { 51 | "siteId": True, 52 | }, 53 | "motionSensors": { 54 | "siteId": True, 55 | }, 56 | "sitePermission": { 57 | "siteId": True, 58 | "userId": True, 59 | "user": True, 60 | "site": True, 61 | }, 62 | } 63 | 64 | T = TypeVar("T", dict, list) 65 | 66 | 67 | def redact(data: T, keys: dict) -> T: 68 | """Recursively redact potentially sensitive information from Plejd Site data.""" 69 | if isinstance(data, list): 70 | return [redact(item, keys) for item in data] 71 | for key, value in keys.items(): 72 | if key in data: 73 | if value is True: 74 | data[key] = "" 75 | else: 76 | data[key] = redact(data[key], value) 77 | return data 78 | 79 | 80 | async def async_get_config_entry_diagnostics( 81 | hass: HomeAssistant, config_entry: ConfigEntry 82 | ): 83 | """Return the plejd site configuration from the cloud.""" 84 | 85 | site: PlejdSite = get_plejd_site_from_config_entry(hass, config_entry) 86 | plejdManager: PlejdManager = site.manager 87 | sitedata = await plejdManager.get_raw_sitedata() 88 | return redact(sitedata, REDACT_KEYS) 89 | -------------------------------------------------------------------------------- /custom_components/plejd/__init__.py: -------------------------------------------------------------------------------- 1 | """Support for Plejd mesh devices.""" 2 | 3 | from homeassistant.config_entries import ConfigEntry 4 | from homeassistant.const import ( 5 | CONF_USERNAME, 6 | CONF_PASSWORD, 7 | EVENT_HOMEASSISTANT_STOP, 8 | Platform, 9 | ) 10 | from homeassistant.core import HomeAssistant 11 | from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady 12 | from homeassistant.helpers.device_registry import DeviceEntry 13 | 14 | from .const import DOMAIN, CONF_SITE_ID 15 | from .plejd_site import PlejdSite, ConnectionError, AuthenticationError 16 | 17 | PLATFORMS = [ 18 | Platform.LIGHT, 19 | Platform.SWITCH, 20 | Platform.SCENE, 21 | Platform.EVENT, 22 | Platform.BINARY_SENSOR, 23 | Platform.COVER, 24 | ] 25 | 26 | 27 | async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: 28 | """Set up a Plejd mesh for a config entry.""" 29 | 30 | hass.data.setdefault(DOMAIN, {}) 31 | 32 | site = hass.data[DOMAIN][config_entry.entry_id] = PlejdSite( 33 | hass, 34 | config_entry, 35 | username=config_entry.data.get(CONF_USERNAME), 36 | password=config_entry.data.get(CONF_PASSWORD), 37 | siteId=config_entry.data.get(CONF_SITE_ID), 38 | ) 39 | 40 | await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) 41 | 42 | try: 43 | await site.start() 44 | except ConnectionError as err: 45 | raise ConfigEntryNotReady from err 46 | except AuthenticationError as err: 47 | raise ConfigEntryAuthFailed from err 48 | 49 | config_entry.async_on_unload( 50 | hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, site.stop) 51 | ) 52 | 53 | return True 54 | 55 | 56 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 57 | """Unload a config entry.""" 58 | 59 | unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) 60 | 61 | if not unload_ok: 62 | return unload_ok 63 | 64 | site: PlejdSite = hass.data[DOMAIN][entry.entry_id] 65 | await site.stop() 66 | del hass.data[DOMAIN][entry.entry_id] 67 | 68 | return unload_ok 69 | 70 | 71 | async def async_remove_config_entry_device( 72 | hass: HomeAssistant, 73 | config_entry: ConfigEntry, 74 | device_entry: DeviceEntry, 75 | ) -> bool: 76 | """Allow removing a Plejd device if orphaned.""" 77 | site: PlejdSite = hass.data[DOMAIN][config_entry.entry_id] 78 | if not site: 79 | return True 80 | for device in site.devices: 81 | if device.hidden: 82 | continue 83 | if device.identifier in device_entry.identifiers: 84 | return False 85 | 86 | return True 87 | -------------------------------------------------------------------------------- /custom_components/plejd/cover.py: -------------------------------------------------------------------------------- 1 | """Support for Plejd covers.""" 2 | 3 | from typing import Any 4 | from homeassistant.components.cover import CoverEntity, CoverEntityFeature 5 | from homeassistant.config_entries import ConfigEntry 6 | from homeassistant.core import callback, HomeAssistant 7 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 8 | 9 | from .plejd_site import ( 10 | dt, 11 | get_plejd_site_from_config_entry, 12 | ) 13 | from .plejd_entity import PlejdDeviceBaseEntity 14 | 15 | 16 | async def async_setup_entry( 17 | hass: HomeAssistant, 18 | config_entry: ConfigEntry, 19 | async_add_entities: AddEntitiesCallback, 20 | ) -> None: 21 | """Set up the Plejd lights from a config entry.""" 22 | site = get_plejd_site_from_config_entry(hass, config_entry) 23 | 24 | @callback 25 | def async_add_cover(device: PlejdCover) -> None: 26 | """Add light from Plejd.""" 27 | entity = PlejdCover(device) 28 | async_add_entities([entity]) 29 | 30 | site.register_platform_add_device_callback( 31 | async_add_cover, dt.PlejdDeviceType.COVER 32 | ) 33 | 34 | 35 | class PlejdCover(PlejdDeviceBaseEntity, CoverEntity): 36 | 37 | def __init__(self, device: dt.PlejdCover) -> None: 38 | """Set up light.""" 39 | CoverEntity.__init__(self) 40 | PlejdDeviceBaseEntity.__init__(self, device) 41 | self.device: PlejdCover 42 | 43 | self._attr_supported_features = ( 44 | CoverEntityFeature.OPEN 45 | | CoverEntityFeature.CLOSE 46 | | CoverEntityFeature.STOP 47 | | CoverEntityFeature.SET_POSITION 48 | # | CoverEntityFeature.SET_TILT_POSITION 49 | ) 50 | 51 | @property 52 | def current_cover_position(self) -> int | None: 53 | return self._data.get("position", 0) 54 | 55 | @property 56 | def current_cover_tilt_position(self) -> int | None: 57 | return None 58 | return self._data.get("angle", None) 59 | 60 | @property 61 | def is_closed(self) -> bool | None: 62 | return self.current_cover_position == 0 63 | 64 | @property 65 | def is_closing(self) -> bool | None: 66 | if not self._data.get("moving"): 67 | return False 68 | return not self._data.get("opening") 69 | 70 | @property 71 | def is_opening(self) -> bool | None: 72 | if not self._data.get("moving"): 73 | return False 74 | return self._data.get("opening") 75 | 76 | async def async_open_cover(self, **kwargs: Any) -> None: 77 | await self.device.open() 78 | 79 | async def async_close_cover(self, **kwargs: Any) -> None: 80 | await self.device.close() 81 | 82 | async def async_stop_cover(self, **kwargs: Any) -> None: 83 | await self.device.stop() 84 | 85 | async def async_set_cover_position( 86 | self, position: int | None = None, **kwargs: Any 87 | ) -> None: 88 | await self.device.set_position(position) 89 | -------------------------------------------------------------------------------- /custom_components/plejd/plejd_entity.py: -------------------------------------------------------------------------------- 1 | """Plejd entity helpers.""" 2 | 3 | from homeassistant.core import callback, HomeAssistant 4 | from homeassistant.helpers.entity import Entity 5 | from homeassistant.helpers import device_registry as dr 6 | 7 | from .const import DOMAIN, MANUFACTURER 8 | from .plejd_site import dt 9 | 10 | 11 | class PlejdDeviceBaseEntity(Entity): 12 | """Representation of a Plejd device.""" 13 | 14 | _attr_has_entity_name = True 15 | _attr_name = None 16 | 17 | def __init__(self, device: dt.PlejdDevice): 18 | """Set up entity.""" 19 | super().__init__() 20 | self.device = device 21 | self.listener = None 22 | self._data = {} 23 | 24 | @property 25 | def device_info(self): 26 | """Return a device description for device registry.""" 27 | return { 28 | "identifiers": {(DOMAIN, *self.device.device_identifier)}, 29 | "name": self.device.name, 30 | "manufacturer": MANUFACTURER, 31 | "model": self.device.hardware, 32 | "suggested_area": self.device.room, 33 | "sw_version": str(self.device.firmware), 34 | } 35 | 36 | @property 37 | def unique_id(self): 38 | """Return unique identifier for the entity.""" 39 | return ":".join(self.device.identifier) 40 | 41 | @property 42 | def entity_registry_visible_default(self): 43 | """Return if the device should be visible by default""" 44 | return not self.device.hidden 45 | 46 | @property 47 | def available(self) -> bool: 48 | """Returns whether the switch is avaiable.""" 49 | return self._data.get("available", False) 50 | 51 | @callback 52 | def _handle_update(self, data) -> None: 53 | """When device state is updated from Plejd""" 54 | pass 55 | 56 | async def async_added_to_hass(self) -> None: 57 | """When entity is added to hass.""" 58 | 59 | def _listener(data): 60 | self._data = data 61 | self._handle_update(data) 62 | self.async_write_ha_state() 63 | 64 | self.listener = self.device.subscribe(_listener) 65 | 66 | async def async_will_remove_from_hass(self) -> None: 67 | """When entity will be removed from hass.""" 68 | if self.listener: 69 | self.listener() 70 | return await super().async_will_remove_from_hass() 71 | 72 | 73 | @callback 74 | def register_unknown_device( 75 | hass: HomeAssistant, device: dt.PlejdDevice, config_entry_id: str 76 | ): 77 | """Add a empty device to the device registry for unknown devices.""" 78 | device_registry = dr.async_get(hass) 79 | device_registry.async_get_or_create( 80 | config_entry_id=config_entry_id, 81 | identifiers={(DOMAIN, *device.device_identifier)}, 82 | manufacturer=MANUFACTURER, 83 | name=device.name, 84 | model=device.hardware, 85 | suggested_area=device.room, 86 | sw_version=str(device.firmware), 87 | ) 88 | -------------------------------------------------------------------------------- /custom_components/plejd/light.py: -------------------------------------------------------------------------------- 1 | """Support for Plejd lights.""" 2 | 3 | from homeassistant.components.light import LightEntity, ColorMode 4 | from homeassistant.config_entries import ConfigEntry 5 | from homeassistant.core import callback, HomeAssistant 6 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 7 | 8 | from .plejd_site import ( 9 | dt, 10 | get_plejd_site_from_config_entry, 11 | ) 12 | from .plejd_entity import PlejdDeviceBaseEntity 13 | 14 | 15 | async def async_setup_entry( 16 | hass: HomeAssistant, 17 | config_entry: ConfigEntry, 18 | async_add_entities: AddEntitiesCallback, 19 | ) -> None: 20 | """Set up the Plejd lights from a config entry.""" 21 | site = get_plejd_site_from_config_entry(hass, config_entry) 22 | 23 | @callback 24 | def async_add_light(device: dt.PlejdLight) -> None: 25 | """Add light from Plejd.""" 26 | entity = PlejdLight(device) 27 | async_add_entities([entity]) 28 | 29 | site.register_platform_add_device_callback( 30 | async_add_light, dt.PlejdDeviceType.LIGHT 31 | ) 32 | 33 | 34 | class PlejdLight(PlejdDeviceBaseEntity, LightEntity): 35 | """Representation of a Plejd light.""" 36 | 37 | def __init__(self, device: dt.PlejdLight) -> None: 38 | """Set up light.""" 39 | LightEntity.__init__(self) 40 | PlejdDeviceBaseEntity.__init__(self, device) 41 | self.device: dt.PlejdLight 42 | 43 | self._attr_supported_color_modes: set[ColorMode] = set() 44 | if device.colortemp: 45 | self._attr_supported_color_modes.add(ColorMode.COLOR_TEMP) 46 | self._attr_min_color_temp_kelvin = device.colortemp[0] 47 | self._attr_max_color_temp_kelvin = device.colortemp[1] 48 | elif device.dimmable: 49 | self._attr_supported_color_modes.add(ColorMode.BRIGHTNESS) 50 | else: 51 | self._attr_supported_color_modes.add(ColorMode.ONOFF) 52 | 53 | @property 54 | def is_on(self) -> bool: 55 | """Returns true if light is on.""" 56 | if self.device.outputType == "COVERABLE": 57 | return True 58 | return self._data.get("state", False) 59 | 60 | @property 61 | def brightness(self) -> int | None: 62 | """Returns the current brightness of the light.""" 63 | return self._data.get("dim", 0) 64 | 65 | @property 66 | def color_temp_kelvin(self) -> int | None: 67 | """Returns the current color temperature of the light.""" 68 | return self._data.get("colortemp", None) 69 | 70 | @property 71 | def color_mode(self) -> str: 72 | """Returns the current color mode of the light.""" 73 | if self.device.colortemp: 74 | return ColorMode.COLOR_TEMP 75 | if self.device.dimmable: 76 | return ColorMode.BRIGHTNESS 77 | return ColorMode.ONOFF 78 | 79 | async def async_turn_on( 80 | self, brightness: int | None = None, color_temp: int | None = None, **_ 81 | ) -> None: 82 | """Turn the light on.""" 83 | await self.device.turn_on(brightness, color_temp) 84 | 85 | async def async_turn_off(self, **_) -> None: 86 | """Turn the light off.""" 87 | await self.device.turn_off() 88 | -------------------------------------------------------------------------------- /custom_components/plejd/event.py: -------------------------------------------------------------------------------- 1 | """Support for Plejd events.""" 2 | 3 | from datetime import timedelta 4 | 5 | from homeassistant.components.event import EventEntity, EventDeviceClass 6 | from homeassistant.config_entries import ConfigEntry 7 | from homeassistant.core import callback, HomeAssistant 8 | from homeassistant.util import Throttle 9 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 10 | 11 | from .plejd_site import ( 12 | dt, 13 | get_plejd_site_from_config_entry, 14 | ) 15 | from .plejd_entity import PlejdDeviceBaseEntity 16 | 17 | import logging 18 | 19 | _LOGGER = logging.getLogger(__name__) 20 | 21 | SCENE_ACTIVATION_RATE_LIMIT = timedelta(seconds=2) 22 | 23 | 24 | async def async_setup_entry( 25 | hass: HomeAssistant, 26 | config_entry: ConfigEntry, 27 | async_add_entities: AddEntitiesCallback, 28 | ): 29 | """Set up the Plejd events from a config entry.""" 30 | site = get_plejd_site_from_config_entry(hass, config_entry) 31 | 32 | @callback 33 | def async_add_button_event(device: dt.PlejdButton): 34 | """Add button events from Plejd.""" 35 | entity = PlejdButtonEvent(device) 36 | async_add_entities([entity]) 37 | 38 | site.register_platform_add_device_callback( 39 | async_add_button_event, dt.PlejdDeviceType.BUTTON 40 | ) 41 | 42 | @callback 43 | def async_add_scene_event(scene: dt.PlejdScene): 44 | entity = PlejdSceneEvent(scene) 45 | async_add_entities([entity]) 46 | 47 | site.register_platform_add_device_callback( 48 | async_add_scene_event, dt.PlejdDeviceType.SCENE 49 | ) 50 | 51 | 52 | class PlejdSceneEvent(PlejdDeviceBaseEntity, EventEntity): 53 | """Event for scenes triggered in Plejd.""" 54 | 55 | _attr_has_entity_name = True 56 | _attr_event_types = ["activated"] 57 | device_info = None 58 | 59 | def __init__(self, device: dt.PlejdScene) -> None: 60 | """Set up event.""" 61 | super().__init__(device) 62 | self.device: dt.PlejdScene 63 | 64 | @property 65 | def name(self) -> str: 66 | """Return name of the event entity.""" 67 | return self.device.name + " activated" 68 | 69 | @property 70 | def unique_id(self) -> str: 71 | """Return unique identifier for the event entity.""" 72 | return super().unique_id + ":activated" 73 | 74 | @Throttle(SCENE_ACTIVATION_RATE_LIMIT) 75 | @callback 76 | def _handle_update(self, event) -> None: 77 | """When scene is activated from Plejd.""" 78 | if event.get("triggered", False): 79 | self._trigger_event("activated") 80 | 81 | 82 | class PlejdButtonEvent(PlejdDeviceBaseEntity, EventEntity): 83 | """Event for button presses in Plejd.""" 84 | 85 | _attr_has_entity_name = True 86 | _attr_event_types = ["press", "release"] 87 | _attr_device_class = EventDeviceClass.BUTTON 88 | 89 | def __init__(self, device: dt.PlejdButton) -> None: 90 | """Set up event.""" 91 | super().__init__(device) 92 | self.device: dt.PlejdButton 93 | 94 | @property 95 | def name(self) -> str: 96 | """Return the name of the event entity.""" 97 | return f"{self.device.button_id+1} pressed" 98 | 99 | @property 100 | def unique_id(self) -> str: 101 | """Return unique identifier for the event entity.""" 102 | return super().unique_id + ":press" 103 | 104 | # @Throttle(SCENE_ACTIVATION_RATE_LIMIT) 105 | @callback 106 | def _handle_update(self, event) -> None: 107 | """When a button is pushed from Plejd.""" 108 | action = event.get("action") 109 | if action == "press": 110 | self._trigger_event("press") 111 | elif action == "release": 112 | self._trigger_event("release") 113 | -------------------------------------------------------------------------------- /custom_components/plejd/config_flow.py: -------------------------------------------------------------------------------- 1 | """Config flow for Plejd integration 2 | 3 | Provides user initiated configuration flow. 4 | Discovery of Plejd mesh devices through Bluetooth. 5 | Reauthentication when issues with cloud api credentials are reported. 6 | """ 7 | 8 | import voluptuous as vol 9 | import logging 10 | from typing import Any 11 | from homeassistant.config_entries import ConfigFlow, ConfigEntry, FlowResult 12 | from homeassistant.components import bluetooth 13 | from homeassistant.const import CONF_USERNAME, CONF_PASSWORD 14 | 15 | from pyplejd import get_sites, verify_credentials, AuthenticationError, ConnectionError 16 | from .const import DOMAIN, CONF_SITE_ID, CONF_SITE_TITLE 17 | 18 | _LOGGER = logging.getLogger(__name__) 19 | 20 | 21 | class PlejdConfigFlow(ConfigFlow, domain=DOMAIN): 22 | """Handle a Plejd config flow.""" 23 | 24 | VERSION = 1 25 | 26 | def __init__(self) -> None: 27 | """Initialize the Plejd config flow""" 28 | self.config: dict[str, Any] = {} 29 | self.reauth_config_entry: ConfigEntry | None = None 30 | self.sites: dict[str, str] = {} 31 | 32 | async def async_step_bluetooth(self, _: Any) -> FlowResult: 33 | """Handle a discovered Plejd mesh.""" 34 | if self._async_current_entries(): 35 | return self.async_abort(reason="single_instance_allowed") 36 | 37 | # Several devices may be discovered, but most likely they all belong to the same mesh. 38 | # So this makes sure we only get a single discovery message. 39 | if self._async_in_progress(): 40 | return self.async_abort(reason="single_instance_allowed") 41 | 42 | return await self.async_step_user() 43 | 44 | async def async_step_reauth(self, _: dict[str, Any] | None = None) -> FlowResult: 45 | """Trigger a reauthentication flow.""" 46 | 47 | config_entry = self.hass.config_entries.async_get_entry( 48 | self.context["entry_id"] 49 | ) 50 | assert config_entry 51 | self.reauth_config_entry = config_entry 52 | 53 | return await self.async_step_reauth_confirm() 54 | 55 | async def async_step_reauth_confirm( 56 | self, user_input: dict[str, Any] | None = None 57 | ) -> FlowResult: 58 | if user_input is not None: 59 | return await self.async_step_user() 60 | 61 | return self.async_show_form( 62 | step_id="reauth_confirm", data_schema=vol.Schema({}) 63 | ) 64 | 65 | async def async_step_user( 66 | self, user_input: dict[str, Any] | None = None 67 | ) -> FlowResult: 68 | """Handle a flow initiated by user.""" 69 | 70 | if not bluetooth.async_scanner_count(self.hass, connectable=True): 71 | return self.async_abort(reason="bluetooth_not_available") 72 | 73 | errors = {} 74 | 75 | if user_input is not None: 76 | self.config = { 77 | CONF_USERNAME: user_input[CONF_USERNAME], 78 | CONF_PASSWORD: user_input[CONF_PASSWORD], 79 | } 80 | 81 | try: 82 | await verify_credentials( 83 | username=self.config[CONF_USERNAME], 84 | password=self.config[CONF_PASSWORD], 85 | ) 86 | except AuthenticationError: 87 | errors["base"] = "faulty_credentials" 88 | except ConnectionError: 89 | return self.async_abort(reason="no_cloud_connection") 90 | else: 91 | return await self.async_step_picksite() 92 | 93 | return self.async_show_form( 94 | step_id="user", 95 | data_schema=vol.Schema( 96 | {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} 97 | ), 98 | errors=errors, 99 | ) 100 | 101 | async def async_step_picksite( 102 | self, user_input: dict[str, Any] | None = None 103 | ) -> FlowResult: 104 | """Select Plejd site to control.""" 105 | 106 | if self.reauth_config_entry: 107 | self.config.update( 108 | { 109 | CONF_SITE_ID: self.reauth_config_entry.data[CONF_SITE_ID], 110 | CONF_SITE_TITLE: self.reauth_config_entry.data[CONF_SITE_TITLE], 111 | } 112 | ) 113 | 114 | self.hass.config_entries.async_update_entry( 115 | self.reauth_config_entry, 116 | data=self.config, 117 | ) 118 | await self.hass.config_entries.async_reload( 119 | self.reauth_config_entry.entry_id 120 | ) 121 | 122 | return self.async_abort(reason="reauth_successful") 123 | elif user_input is not None: 124 | siteId = user_input["site"] 125 | 126 | await self.async_set_unique_id(siteId) 127 | self._abort_if_unique_id_configured() 128 | 129 | self.config.update( 130 | { 131 | CONF_SITE_ID: siteId, 132 | CONF_SITE_TITLE: self.sites[siteId], 133 | } 134 | ) 135 | 136 | return self.async_create_entry(title=self.sites[siteId], data=self.config) 137 | 138 | sites = await get_sites( 139 | username=self.config[CONF_USERNAME], password=self.config[CONF_PASSWORD] 140 | ) 141 | 142 | options = {} 143 | for site in sites: 144 | self.sites[site["siteId"]] = site["title"] 145 | options[site["siteId"]] = f"{site["title"]} ({site["deviceCount"]} devices)" 146 | 147 | return self.async_show_form( 148 | step_id="picksite", 149 | data_schema=vol.Schema({vol.Required("site"): vol.In(options)}), 150 | ) 151 | -------------------------------------------------------------------------------- /custom_components/plejd/plejd_site.py: -------------------------------------------------------------------------------- 1 | """Plejd site mesh controller.""" 2 | 3 | from datetime import timedelta 4 | import logging 5 | from typing import cast, Callable 6 | from collections import defaultdict 7 | 8 | from homeassistant.components import bluetooth 9 | from homeassistant.config_entries import ConfigEntry 10 | from homeassistant.core import HomeAssistant 11 | from homeassistant.helpers.event import async_track_time_interval 12 | from homeassistant.helpers.storage import Store 13 | 14 | from home_assistant_bluetooth import BluetoothServiceInfoBleak 15 | 16 | from pyplejd import ( 17 | PlejdManager, 18 | ConnectionError, 19 | AuthenticationError, 20 | PLEJD_SERVICE, 21 | DeviceTypes as dt, 22 | ) 23 | 24 | from .const import DOMAIN 25 | from .plejd_entity import register_unknown_device 26 | 27 | 28 | _LOGGER = logging.getLogger(__name__) 29 | 30 | SITE_DATA_STORE_KEY = "plejd_site_data" 31 | SITE_DATA_STORE_VERSION = 1 32 | 33 | 34 | class PlejdSite: 35 | """Controller for a Plejd site mesh.""" 36 | 37 | def __init__( 38 | self, 39 | hass: HomeAssistant, 40 | config_entry: ConfigEntry, 41 | username: str, 42 | password: str, 43 | siteId: str, 44 | ) -> None: 45 | """Initialize plejd site mesh.""" 46 | self.hass: HomeAssistant = hass 47 | self.config_entry: ConfigEntry = config_entry 48 | 49 | self.credentials = { 50 | "username": username, 51 | "password": password, 52 | "siteId": siteId, 53 | } 54 | 55 | self.store = Store(hass, SITE_DATA_STORE_VERSION, SITE_DATA_STORE_KEY) 56 | 57 | self.manager: PlejdManager = PlejdManager(**self.credentials) 58 | 59 | self.devices: list[dt.PlejdDevice] = [] 60 | 61 | self.started = False 62 | self.stopping = False 63 | 64 | self.add_device_callbacks = defaultdict(list) 65 | 66 | def register_platform_add_device_callback( 67 | self, 68 | callback: Callable[[dt.PlejdDevice], None], 69 | output_type: dt.PlejdDeviceType, 70 | ) -> None: 71 | self.add_device_callbacks[output_type].append(callback) 72 | 73 | async def start(self) -> None: 74 | """Setup and connect to plejd site.""" 75 | if not (site_data_cache := await self.store.async_load()) or not isinstance( 76 | site_data_cache, dict 77 | ): 78 | site_data_cache = {} 79 | 80 | cached_site_data = site_data_cache.get(self.credentials["siteId"]) 81 | 82 | await self.manager.init(cached_site_data) 83 | 84 | self.devices = self.manager.devices 85 | 86 | for device in self.devices: 87 | if adders := self.add_device_callbacks.get(device.outputType): 88 | for adder in adders: 89 | adder(device) 90 | else: 91 | if device.outputType: 92 | register_unknown_device( 93 | self.hass, device, self.config_entry.entry_id 94 | ) 95 | 96 | # Close any stale connections that may be open 97 | for dev in self.devices: 98 | if dev.BLEaddress: 99 | ble_device = bluetooth.async_ble_device_from_address( 100 | self.hass, dev.BLEaddress, True 101 | ) 102 | if ble_device: 103 | await self.manager.close_stale(ble_device) 104 | 105 | # Register callback for bluetooth discover 106 | self.config_entry.async_on_unload( 107 | bluetooth.async_register_callback( 108 | self.hass, 109 | self._discovered, 110 | bluetooth.match.BluetoothCallbackMatcher( 111 | connectable=True, service_uuid=PLEJD_SERVICE.lower() 112 | ), 113 | bluetooth.BluetoothScanningMode.PASSIVE, 114 | ) 115 | ) 116 | 117 | # Run through already discovered devices and add plejds to the manager 118 | for service_info in bluetooth.async_discovered_service_info(self.hass, True): 119 | if PLEJD_SERVICE.lower() in service_info.advertisement.service_uuids: 120 | self._discovered(service_info, connect=False) 121 | 122 | # Ping the mesh periodically to maintain the connection 123 | self.config_entry.async_on_unload( 124 | async_track_time_interval( 125 | self.hass, 126 | self._ping, 127 | self.manager.ping_interval, 128 | name="Plejd keep-alive", 129 | ) 130 | ) 131 | 132 | # Check that the mesh clock is in sync once per hour 133 | self.config_entry.async_on_unload( 134 | async_track_time_interval( 135 | self.hass, 136 | self._broadcast_time, 137 | timedelta(hours=1), 138 | name="Plejd sync time", 139 | ) 140 | ) 141 | 142 | self.started = True 143 | 144 | self.hass.async_create_task(self._ping()) 145 | self.hass.async_create_task(self._broadcast_time()) 146 | 147 | async def stop(self, *_) -> None: 148 | """Disconnect mesh and tear down site configuration.""" 149 | self.stopping = True 150 | 151 | if not (site_data_cache := await self.store.async_load()) or not isinstance( 152 | site_data_cache, dict 153 | ): 154 | site_data_cache = {} 155 | site_data_cache[self.credentials["siteId"]] = ( 156 | await self.manager.get_raw_sitedata() 157 | ) 158 | await self.store.async_save(site_data_cache) 159 | 160 | await self.manager.disconnect() 161 | 162 | def _discovered( 163 | self, service_info: BluetoothServiceInfoBleak, *_, connect: bool = True 164 | ) -> None: 165 | """Register any discovered plejd device with the manager.""" 166 | new_device = self.manager.add_mesh_device( 167 | service_info.device, service_info.rssi 168 | ) 169 | if connect and new_device: 170 | self.hass.async_create_task(self._ping()) 171 | 172 | async def _ping(self, *_) -> None: 173 | """Ping the plejd mesh to connect or maintain the connection.""" 174 | if self.stopping or not self.started: 175 | return 176 | if not await self.manager.ping(): 177 | _LOGGER.debug("Ping failed") 178 | 179 | async def _broadcast_time(self, *_) -> None: 180 | """Check that the mesh clock is in sync.""" 181 | if self.stopping: 182 | return 183 | await self.manager.broadcast_time() 184 | 185 | 186 | def get_plejd_site_from_config_entry( 187 | hass: HomeAssistant, config_entry: ConfigEntry 188 | ) -> PlejdSite: 189 | """Get the Plejd site corresponding to a config entry.""" 190 | return cast(PlejdSite, hass.data[DOMAIN].get(config_entry.entry_id)) 191 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Plejd integration for Home Assistant 2 | 3 | 4 | 5 | Connects to your [Plejd](https://www.plejd.com) devices using your Home Assistant Bluetooth. 6 | 7 | This integration requires a [Bluetooth](https://www.home-assistant.io/integrations/bluetooth/) adapter which supports at least one Active connections. 8 | 9 | Using an [EspHome Bluetooth Proxy](https://esphome.io/projects/?type=bluetooth) is recommended, but Shelly proxies will not work. 10 | If you make your own esphome configuration, make sure the [`bluetooth_proxy`](https://esphome.io/components/bluetooth_proxy) has `active` set to `True`. 11 | 12 | ## Installation 13 | 14 | - Make sure you have a working Bluetooth integration in Home Assistant (a bluetooth proxy should work too) 15 | - Make sure you have no other plejd custom components or add-ons running. 16 | - Install the integration: 17 | - Using HACS: [![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=thomasloven&repository=hass-plejd&category=integration) or search for `Plejd`. 18 | - Manually: Download the `plejd` directory and place in your `/custom_components`. 19 | 20 | - Restart Home Assistant 21 | - Hopefully, your Plejd mesh will be auto discovered and you should see a message popping up in your integrations page. 22 | - Log in with the credentials you use in the Plejd app when prompted (email address and password) 23 | 24 | ## Supported devices 25 | 26 | - All known Plejd lights, dimmers and relays should work. 27 | 28 | - All buttons connected to a device or WPH-01 should work and register events when the buttons are pressed. 29 | 30 | - If the button is held for a while, a release even will be registered too. The required hold time doesn't seem entirly consistent, though... 31 | 32 | - Rotary dimmer WRT-01 should register and fire events when pushed. 33 | 34 | - Rotations are **not** registered. This is a limitation in how Plejd works. Rotation events are not actually sent to the mesh, but directly to whatever device the WRT-01 is paired to. Therefore it is impossible to listen in on them. 35 | 36 | - Plejd Scenes should show up and be triggerable in Home Assistant (unless hidden in the Plejd app). 37 | - An event entity will be triggered when they are activated (even if hidden in the Plejd app). 38 | 39 | ## Unsupported devices 40 | 41 | - GWY-01 doesn't do anything. 42 | 43 | - EXT-01 doesn't do anything 44 | 45 | - RTR-01 Is not actually a device but an addition to other devices. 46 | 47 | ## Cloud connection 48 | 49 | The integration will fetch the device list and - most importantly - the cryptographic keys for the BLE communication from the Plejd cloud at launch. After that initial download, no communication is made with the cloud. All controll is local over bluetooth. 50 | 51 | ## Debug logging 52 | 53 | There are some loggers which may be useful for troubleshooting. They are used by adding the following to your `configuration.yaml` (pick the `pyplejd.` ones which are relevant to you): 54 | 55 | ```yaml 56 | logger: 57 | default: warning 58 | logs: 59 | pyplejd.device_list: debug # Will output the list of devices pulled from the cloud - also shows the mesh index and ble address 60 | pyplejd.ble.connection: debug # Will show the process of connecting to the BLE mesh - this could give information if all your devices are shown but not available 61 | pyplejd.ble.device.123: debug # Will show the BLE traffic to and from the device with mesh index 123 - you can find the mesh index from the Plejd app or from the device_list debug output above 62 | pyplejd.ble.device.SCN: debug # Will show the BLE traffic related to Scenes 63 | pyplejd.ble.device.TME: debug # Will show the BLE traffic related to timekeeping 64 | pyplejd.ble.device.all: debug # Will show the BLE traffic to and from ALL devices 65 | ``` 66 | 67 | ## Other integrations 68 | 69 | There area several other integrations for Plejd with Home Assistant available, made by some awesome people. 70 | 71 | The following is a list of the ones I have looked at in order to create this one, and how this one is different. 72 | 73 | I could not have made this one without their great job in decoding the Plejd cloud API and Bluetooth communication protocol. 74 | 75 | | | | | 76 | | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 77 | | [hassio-plejd](https://github.com/icanos/hassio-plejd) | [@icanos](https://github.com/icanos) | Works only with Home Assistant OS.
Relies on MQTT for communication.
Requires exclusive access to a Bluetooth dongle.
Does not support Bluetooth Proxy. | 78 | | [plejd2mqtt](https://github.com/thomasloven/plejd2mqtt) | [@thomasloven](https://github.com/thomasloven) | Somewhat outdated stand-alone version of the above.
Relies on MQTT for communication.
Requires exclusive access to a Bluetooth dongle.
Does not support Bluetooth Proxy.
Does not support switches or scenes. | 79 | | [ha-plejd](https://github.com/klali/ha-plejd) | [@klali](https://github.com/klali)
(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.
No auto discovery.
Requires exclusive access to a Bluetooth dongle.
Does not support Bluetooth Proxy. | 80 | | [homey-plejd](https://github.com/emilohman/homey-plejd) | [@emilohman](https://github.com/emilohman) | For Homey | 81 | | [homebridge-plejd](https://github.com/blommegard/homebridge-plejd) | [@blommegard](https://github.com/blommegard) | For Homebridge | 82 | 83 | The Plejd name and logo is copyrighted and trademarked and belongs to Plejd AB. \ 84 | The author of this repository is not associated with Plejd AB. 85 | 86 | > Sidenote: This integration makes use of a cloud API made by Plejd for use by their smartphone apps. 87 | > The use of the API in this integration is not endorsed by Plejd, but I have been in communication with representatives of the company, and they have informally indicated an intent to look the other way as long as the API usage does not become excessive. 88 | > 89 | > In those days of clouds locking down and free services becoming paid, this is fantastic! 90 | > 91 | > Thanks, Plejd! 92 | --------------------------------------------------------------------------------