2021-03-05 02:23:28 +01:00
|
|
|
import asyncio
|
|
|
|
from threading import RLock
|
2023-02-02 23:21:12 +01:00
|
|
|
from typing import Any, Dict, Iterable, List, Optional, Tuple, Type, Union
|
2021-03-05 02:23:28 +01:00
|
|
|
|
2023-01-26 22:10:02 +01:00
|
|
|
import aiohttp
|
2023-01-24 23:56:28 +01:00
|
|
|
from pysmartthings import (
|
|
|
|
Attribute,
|
|
|
|
Capability,
|
|
|
|
Command,
|
2023-01-26 22:10:02 +01:00
|
|
|
DeviceEntity,
|
2023-01-24 23:56:28 +01:00
|
|
|
DeviceStatus,
|
2023-02-02 23:21:12 +01:00
|
|
|
Location,
|
|
|
|
Room,
|
2023-01-24 23:56:28 +01:00
|
|
|
SmartThings,
|
|
|
|
)
|
2023-01-22 00:09:10 +01:00
|
|
|
|
2023-02-02 23:21:12 +01:00
|
|
|
from platypush.entities import (
|
|
|
|
DimmerEntityManager,
|
|
|
|
Entity,
|
|
|
|
EnumSwitchEntityManager,
|
|
|
|
LightEntityManager,
|
|
|
|
SensorEntityManager,
|
|
|
|
SwitchEntityManager,
|
|
|
|
)
|
2023-01-26 22:10:02 +01:00
|
|
|
from platypush.entities.devices import Device
|
2023-01-21 14:09:26 +01:00
|
|
|
from platypush.entities.dimmers import Dimmer
|
2022-05-30 09:23:05 +02:00
|
|
|
from platypush.entities.lights import Light
|
2023-01-22 00:09:10 +01:00
|
|
|
from platypush.entities.sensors import Sensor
|
2023-01-26 22:10:02 +01:00
|
|
|
from platypush.entities.switches import EnumSwitch, Switch
|
2023-01-22 00:09:10 +01:00
|
|
|
from platypush.plugins import RunnablePlugin, action
|
|
|
|
from platypush.utils import camel_case_to_snake_case
|
2021-03-05 02:23:28 +01:00
|
|
|
|
2023-01-26 22:10:02 +01:00
|
|
|
from ._mappers import DeviceMapper, device_mappers
|
2021-03-05 02:23:28 +01:00
|
|
|
|
2023-01-24 23:56:28 +01:00
|
|
|
|
2023-02-02 23:21:12 +01:00
|
|
|
class SmartthingsPlugin(
|
|
|
|
RunnablePlugin,
|
|
|
|
DimmerEntityManager,
|
|
|
|
EnumSwitchEntityManager,
|
|
|
|
LightEntityManager,
|
|
|
|
SensorEntityManager,
|
|
|
|
SwitchEntityManager,
|
|
|
|
):
|
2021-03-05 02:23:28 +01:00
|
|
|
"""
|
|
|
|
Plugin to interact with devices and locations registered to a Samsung SmartThings account.
|
|
|
|
|
|
|
|
Requires:
|
|
|
|
|
|
|
|
* **pysmartthings** (``pip install pysmartthings``)
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
2022-04-04 22:41:04 +02:00
|
|
|
_timeout = aiohttp.ClientTimeout(total=20.0)
|
2021-03-05 02:23:28 +01:00
|
|
|
|
2023-01-22 00:09:10 +01:00
|
|
|
def __init__(
|
|
|
|
self, access_token: str, poll_interval: Optional[float] = 20.0, **kwargs
|
|
|
|
):
|
2021-03-05 02:23:28 +01:00
|
|
|
"""
|
|
|
|
:param access_token: SmartThings API access token - you can get one at https://account.smartthings.com/tokens.
|
2023-01-22 00:09:10 +01:00
|
|
|
:param poll_interval: How often the plugin should poll for changes, in seconds (default: 20).
|
2021-03-05 02:23:28 +01:00
|
|
|
"""
|
2023-01-22 00:09:10 +01:00
|
|
|
super().__init__(poll_interval=poll_interval, **kwargs)
|
2021-03-05 02:23:28 +01:00
|
|
|
self._access_token = access_token
|
|
|
|
self._refresh_lock = RLock()
|
|
|
|
self._execute_lock = RLock()
|
|
|
|
|
2023-02-02 23:21:12 +01:00
|
|
|
self._locations: List[Location] = []
|
|
|
|
self._devices: List[DeviceEntity] = []
|
|
|
|
self._rooms_by_location: Dict[str, Room] = {}
|
|
|
|
|
|
|
|
self._locations_by_id: Dict[str, Location] = {}
|
|
|
|
self._locations_by_name: Dict[str, Location] = {}
|
|
|
|
self._devices_by_id: Dict[str, DeviceEntity] = {}
|
|
|
|
self._devices_by_name: Dict[str, DeviceEntity] = {}
|
|
|
|
self._rooms_by_id: Dict[str, Room] = {}
|
|
|
|
self._rooms_by_location_and_id: Dict[str, Dict[str, Room]] = {}
|
|
|
|
self._rooms_by_location_and_name: Dict[str, Dict[str, Room]] = {}
|
2023-01-24 23:56:28 +01:00
|
|
|
self._entities_by_id: Dict[str, Entity] = {}
|
2021-03-05 02:23:28 +01:00
|
|
|
|
|
|
|
async def _refresh_locations(self, api):
|
|
|
|
self._locations = await api.locations()
|
2022-04-04 22:41:04 +02:00
|
|
|
self._locations_by_id = {loc.location_id: loc for loc in self._locations}
|
|
|
|
self._locations_by_name = {loc.name: loc for loc in self._locations}
|
2021-03-05 02:23:28 +01:00
|
|
|
|
|
|
|
async def _refresh_devices(self, api):
|
|
|
|
self._devices = await api.devices()
|
2022-04-04 22:41:04 +02:00
|
|
|
self._devices_by_id = {dev.device_id: dev for dev in self._devices}
|
|
|
|
self._devices_by_name = {dev.label: dev for dev in self._devices}
|
2021-03-05 02:23:28 +01:00
|
|
|
|
|
|
|
async def _refresh_rooms(self, api, location_id: str):
|
|
|
|
self._rooms_by_location[location_id] = await api.rooms(location_id=location_id)
|
|
|
|
|
2022-04-04 22:41:04 +02:00
|
|
|
self._rooms_by_id.update(
|
|
|
|
**{room.room_id: room for room in self._rooms_by_location[location_id]}
|
|
|
|
)
|
2021-03-05 02:23:28 +01:00
|
|
|
|
|
|
|
self._rooms_by_location_and_id[location_id] = {
|
2022-04-04 22:41:04 +02:00
|
|
|
room.room_id: room for room in self._rooms_by_location[location_id]
|
2021-03-05 02:23:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
self._rooms_by_location_and_name[location_id] = {
|
2022-04-04 22:41:04 +02:00
|
|
|
room.name: room for room in self._rooms_by_location[location_id]
|
2021-03-05 02:23:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async def _refresh_info(self):
|
|
|
|
async with aiohttp.ClientSession(timeout=self._timeout) as session:
|
2023-01-24 23:56:28 +01:00
|
|
|
api = SmartThings(session, self._access_token)
|
2021-03-05 02:23:28 +01:00
|
|
|
tasks = [
|
|
|
|
asyncio.ensure_future(self._refresh_locations(api)),
|
|
|
|
asyncio.ensure_future(self._refresh_devices(api)),
|
|
|
|
]
|
|
|
|
|
|
|
|
await asyncio.gather(*tasks)
|
|
|
|
|
|
|
|
room_tasks = [
|
|
|
|
asyncio.ensure_future(self._refresh_rooms(api, location.location_id))
|
|
|
|
for location in self._locations
|
|
|
|
]
|
|
|
|
|
|
|
|
await asyncio.gather(*room_tasks)
|
|
|
|
|
|
|
|
def refresh_info(self):
|
|
|
|
with self._refresh_lock:
|
|
|
|
loop = asyncio.new_event_loop()
|
|
|
|
try:
|
|
|
|
asyncio.set_event_loop(loop)
|
|
|
|
loop.run_until_complete(self._refresh_info())
|
|
|
|
finally:
|
|
|
|
loop.stop()
|
|
|
|
|
|
|
|
def _location_to_dict(self, location) -> Dict:
|
|
|
|
return {
|
|
|
|
'name': location.name,
|
|
|
|
'location_id': location.location_id,
|
|
|
|
'country_code': location.country_code,
|
|
|
|
'locale': location.locale,
|
|
|
|
'latitude': location.latitude,
|
|
|
|
'longitude': location.longitude,
|
|
|
|
'temperature_scale': location.temperature_scale,
|
|
|
|
'region_radius': location.region_radius,
|
|
|
|
'timezone_id': location.timezone_id,
|
|
|
|
'rooms': {
|
|
|
|
room.room_id: self._room_to_dict(room)
|
|
|
|
for room in self._rooms_by_location.get(location.location_id, {})
|
2022-04-04 22:41:04 +02:00
|
|
|
},
|
2021-03-05 02:23:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _device_to_dict(device) -> Dict:
|
|
|
|
return {
|
|
|
|
'capabilities': device.capabilities,
|
|
|
|
'name': device.label,
|
|
|
|
'device_id': device.device_id,
|
|
|
|
'location_id': device.location_id,
|
|
|
|
'room_id': device.room_id,
|
|
|
|
'device_type_id': device.device_type_id,
|
|
|
|
'device_type_name': device.device_type_name,
|
|
|
|
'device_type_network': device.device_type_network,
|
|
|
|
}
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _room_to_dict(room) -> Dict:
|
|
|
|
return {
|
|
|
|
'name': room.name,
|
|
|
|
'background_image': room.background_image,
|
|
|
|
'room_id': room.room_id,
|
|
|
|
'location_id': room.location_id,
|
|
|
|
}
|
|
|
|
|
|
|
|
@action
|
|
|
|
def info(self) -> Dict[str, Dict[str, dict]]:
|
|
|
|
"""
|
|
|
|
Return the objects registered to the account, including locations and devices.
|
|
|
|
|
|
|
|
.. code-block:: json
|
|
|
|
|
|
|
|
{
|
|
|
|
"devices": {
|
|
|
|
"smart-tv-id": {
|
|
|
|
"capabilities": [
|
|
|
|
"ocf",
|
|
|
|
"switch",
|
|
|
|
"audioVolume",
|
|
|
|
"audioMute",
|
|
|
|
"tvChannel",
|
|
|
|
"mediaInputSource",
|
|
|
|
"mediaPlayback",
|
|
|
|
"mediaTrackControl",
|
|
|
|
"custom.error",
|
|
|
|
"custom.picturemode",
|
|
|
|
"custom.soundmode",
|
|
|
|
"custom.accessibility",
|
|
|
|
"custom.launchapp",
|
|
|
|
"custom.recording",
|
|
|
|
"custom.tvsearch",
|
|
|
|
"custom.disabledCapabilities",
|
|
|
|
"samsungvd.ambient",
|
|
|
|
"samsungvd.ambientContent",
|
|
|
|
"samsungvd.ambient18",
|
|
|
|
"samsungvd.mediaInputSource",
|
|
|
|
"refresh",
|
|
|
|
"execute",
|
|
|
|
"samsungvd.firmwareVersion",
|
|
|
|
"samsungvd.supportsPowerOnByOcf"
|
|
|
|
],
|
|
|
|
"device_id": "smart-tv-id",
|
|
|
|
"device_type_id": null,
|
|
|
|
"device_type_name": null,
|
|
|
|
"device_type_network": null,
|
|
|
|
"location_id": "location-id",
|
|
|
|
"name": "Samsung Smart TV",
|
|
|
|
"room_id": "room-1"
|
|
|
|
},
|
|
|
|
"tv-switch-id": {
|
|
|
|
"capabilities": [
|
|
|
|
"switch",
|
|
|
|
"refresh",
|
|
|
|
"healthCheck"
|
|
|
|
],
|
|
|
|
"device_id": "tv-switch-id",
|
|
|
|
"device_type_id": null,
|
|
|
|
"device_type_name": null,
|
|
|
|
"device_type_network": null,
|
|
|
|
"location_id": "location-id",
|
|
|
|
"name": "TV Smart Switch",
|
|
|
|
"room_id": "room-1"
|
|
|
|
},
|
|
|
|
"lights-switch-id": {
|
|
|
|
"capabilities": [
|
|
|
|
"switch",
|
|
|
|
"refresh",
|
|
|
|
"healthCheck"
|
|
|
|
],
|
|
|
|
"device_id": "lights-switch-id",
|
|
|
|
"device_type_id": null,
|
|
|
|
"device_type_name": null,
|
|
|
|
"device_type_network": null,
|
|
|
|
"location_id": "location-id",
|
|
|
|
"name": "Lights Switch",
|
|
|
|
"room_id": "room-2"
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"locations": {
|
|
|
|
"location-id": {
|
|
|
|
"name": "My home",
|
|
|
|
"location_id": "location-id",
|
|
|
|
"country_code": "us",
|
|
|
|
"locale": "en-US",
|
|
|
|
"latitude": "latitude",
|
|
|
|
"longitude": "longitude",
|
|
|
|
"temperature_scale": null,
|
|
|
|
"region_radius": null,
|
|
|
|
"timezone_id": null,
|
|
|
|
"rooms": {
|
|
|
|
"room-1": {
|
|
|
|
"background_image": null,
|
|
|
|
"location_id": "location-1",
|
|
|
|
"name": "Living Room",
|
|
|
|
"room_id": "room-1"
|
|
|
|
},
|
|
|
|
"room-2": {
|
|
|
|
"background_image": null,
|
|
|
|
"location_id": "location-1",
|
|
|
|
"name": "Bedroom",
|
|
|
|
"room_id": "room-2"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
"""
|
|
|
|
self.refresh_info()
|
|
|
|
return {
|
2022-04-04 22:41:04 +02:00
|
|
|
'locations': {
|
|
|
|
loc.location_id: self._location_to_dict(loc) for loc in self._locations
|
|
|
|
},
|
|
|
|
'devices': {
|
|
|
|
dev.device_id: self._device_to_dict(dev) for dev in self._devices
|
|
|
|
},
|
2021-03-05 02:23:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
@action
|
2022-04-04 22:41:04 +02:00
|
|
|
def get_location(
|
|
|
|
self, location_id: Optional[str] = None, name: Optional[str] = None
|
|
|
|
) -> dict:
|
2021-03-05 02:23:28 +01:00
|
|
|
"""
|
|
|
|
Get the info of a location by ID or name.
|
|
|
|
|
|
|
|
.. code-block:: json
|
|
|
|
|
|
|
|
{
|
|
|
|
"name": "My home",
|
|
|
|
"location_id": "location-id",
|
|
|
|
"country_code": "us",
|
|
|
|
"locale": "en-US",
|
|
|
|
"latitude": "latitude",
|
|
|
|
"longitude": "longitude",
|
|
|
|
"temperature_scale": null,
|
|
|
|
"region_radius": null,
|
|
|
|
"timezone_id": null,
|
|
|
|
"rooms": {
|
|
|
|
"room-1": {
|
|
|
|
"background_image": null,
|
|
|
|
"location_id": "location-1",
|
|
|
|
"name": "Living Room",
|
|
|
|
"room_id": "room-1"
|
|
|
|
},
|
|
|
|
"room-2": {
|
|
|
|
"background_image": null,
|
|
|
|
"location_id": "location-1",
|
|
|
|
"name": "Bedroom",
|
|
|
|
"room_id": "room-2"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
"""
|
|
|
|
assert location_id or name, 'Specify either location_id or name'
|
2022-04-04 22:41:04 +02:00
|
|
|
if (
|
|
|
|
location_id not in self._locations_by_id
|
|
|
|
or name not in self._locations_by_name
|
|
|
|
):
|
2021-03-05 02:23:28 +01:00
|
|
|
self.refresh_info()
|
|
|
|
|
2023-02-02 23:21:12 +01:00
|
|
|
location: Optional[Dict[str, Any]] = {}
|
|
|
|
if location_id:
|
|
|
|
location = self._locations_by_id.get(location_id)
|
|
|
|
elif name:
|
|
|
|
location = self._locations_by_name.get(name)
|
|
|
|
|
|
|
|
assert location, f'Location {location_id or name} not found'
|
2021-03-05 02:23:28 +01:00
|
|
|
return self._location_to_dict(location)
|
|
|
|
|
2023-01-26 22:10:02 +01:00
|
|
|
def _get_device(self, device: str) -> DeviceEntity:
|
2022-05-30 09:23:05 +02:00
|
|
|
return self._get_devices(device)[0]
|
|
|
|
|
2023-01-24 23:56:28 +01:00
|
|
|
@staticmethod
|
|
|
|
def _to_device_and_property(device: str) -> Tuple[str, Optional[str]]:
|
|
|
|
tokens = device.split(':')
|
|
|
|
if len(tokens) > 1:
|
2023-02-02 23:21:12 +01:00
|
|
|
return (tokens[0], tokens[1])
|
2023-01-24 23:56:28 +01:00
|
|
|
return tokens[0], None
|
|
|
|
|
|
|
|
def _get_existing_and_missing_devices(
|
2023-01-21 14:09:26 +01:00
|
|
|
self, *devices: str
|
2023-01-26 22:10:02 +01:00
|
|
|
) -> Tuple[List[DeviceEntity], List[str]]:
|
2023-01-21 14:09:26 +01:00
|
|
|
# Split the external_id:type indicators and always return the parent device
|
2023-01-24 23:56:28 +01:00
|
|
|
devices = tuple(self._to_device_and_property(dev)[0] for dev in devices)
|
2023-01-21 14:09:26 +01:00
|
|
|
|
|
|
|
found_devs = {
|
|
|
|
dev: self._devices_by_id.get(dev, self._devices_by_name.get(dev))
|
|
|
|
for dev in devices
|
|
|
|
if self._devices_by_id.get(dev, self._devices_by_name.get(dev))
|
|
|
|
}
|
|
|
|
|
|
|
|
missing_devs = {dev for dev in devices if dev not in found_devs}
|
|
|
|
return list(found_devs.values()), list(missing_devs) # type: ignore
|
|
|
|
|
2023-01-26 22:10:02 +01:00
|
|
|
def _get_devices(self, *devices: str) -> List[DeviceEntity]:
|
2023-01-24 23:56:28 +01:00
|
|
|
devs, missing_devs = self._get_existing_and_missing_devices(*devices)
|
2022-05-30 09:23:05 +02:00
|
|
|
if missing_devs:
|
2021-03-05 02:23:28 +01:00
|
|
|
self.refresh_info()
|
|
|
|
|
2023-01-24 23:56:28 +01:00
|
|
|
devs, missing_devs = self._get_existing_and_missing_devices(*devices)
|
2022-05-30 09:23:05 +02:00
|
|
|
assert not missing_devs, f'Devices not found: {missing_devs}'
|
|
|
|
return devs
|
2021-03-05 02:23:28 +01:00
|
|
|
|
|
|
|
@action
|
|
|
|
def get_device(self, device: str) -> dict:
|
|
|
|
"""
|
|
|
|
Get a device info by ID or name.
|
|
|
|
|
|
|
|
:param device: Device ID or name.
|
|
|
|
:return:
|
|
|
|
|
|
|
|
.. code-block:: json
|
|
|
|
|
|
|
|
"tv-switch-id": {
|
|
|
|
"capabilities": [
|
|
|
|
"switch",
|
|
|
|
"refresh",
|
|
|
|
"healthCheck"
|
|
|
|
],
|
|
|
|
"device_id": "tv-switch-id",
|
|
|
|
"device_type_id": null,
|
|
|
|
"device_type_name": null,
|
|
|
|
"device_type_network": null,
|
|
|
|
"location_id": "location-id",
|
|
|
|
"name": "TV Smart Switch",
|
|
|
|
"room_id": "room-1"
|
|
|
|
}
|
|
|
|
|
|
|
|
"""
|
2023-01-26 22:10:02 +01:00
|
|
|
dev = self._get_device(device)
|
|
|
|
return self._device_to_dict(dev)
|
2021-03-05 02:23:28 +01:00
|
|
|
|
2022-04-04 22:41:04 +02:00
|
|
|
async def _execute(
|
|
|
|
self,
|
|
|
|
device_id: str,
|
|
|
|
capability: str,
|
|
|
|
command,
|
|
|
|
component_id: str,
|
|
|
|
args: Optional[list],
|
|
|
|
):
|
2021-03-05 02:23:28 +01:00
|
|
|
async with aiohttp.ClientSession(timeout=self._timeout) as session:
|
2023-01-24 23:56:28 +01:00
|
|
|
api = SmartThings(session, self._access_token)
|
2021-03-05 02:23:28 +01:00
|
|
|
device = await api.device(device_id)
|
2022-04-04 22:41:04 +02:00
|
|
|
ret = await device.command(
|
|
|
|
component_id=component_id,
|
|
|
|
capability=capability,
|
|
|
|
command=command,
|
|
|
|
args=args,
|
|
|
|
)
|
|
|
|
|
2023-01-24 23:56:28 +01:00
|
|
|
assert (
|
|
|
|
ret
|
2023-02-02 23:21:12 +01:00
|
|
|
), f'The command {capability}={command} failed on device {device_id}'
|
2023-01-24 23:56:28 +01:00
|
|
|
|
|
|
|
await self._get_device_status(api, device_id, publish_entities=True)
|
2021-03-05 02:23:28 +01:00
|
|
|
|
|
|
|
@action
|
2022-04-04 22:41:04 +02:00
|
|
|
def execute(
|
|
|
|
self,
|
|
|
|
device: str,
|
|
|
|
capability: str,
|
|
|
|
command,
|
|
|
|
component_id: str = 'main',
|
|
|
|
args: Optional[list] = None,
|
|
|
|
):
|
2021-03-05 02:23:28 +01:00
|
|
|
"""
|
|
|
|
Execute a command on a device.
|
|
|
|
|
|
|
|
Example request to turn on a device with ``switch`` capability:
|
|
|
|
|
|
|
|
.. code-block:: json
|
|
|
|
|
|
|
|
{
|
|
|
|
"type": "request",
|
|
|
|
"action": "smartthings.execute",
|
|
|
|
"args": {
|
|
|
|
"device": "My Switch",
|
|
|
|
"capability": "switch",
|
|
|
|
"command": "on"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
:param device: Device ID or name.
|
|
|
|
:param capability: Property to be read/written (see device ``capabilities`` returned from :meth:`.get_device`).
|
|
|
|
:param command: Command to execute on the ``capability``
|
|
|
|
(see https://smartthings.developer.samsung.com/docs/api-ref/capabilities.html).
|
|
|
|
:param component_id: ID of the component to execute the command on (default: ``main``, i.e. the device itself).
|
|
|
|
:param args: Command extra arguments, as a list.
|
|
|
|
"""
|
2023-01-24 23:56:28 +01:00
|
|
|
dev = self._get_device(device)
|
2021-03-05 02:23:28 +01:00
|
|
|
|
|
|
|
with self._execute_lock:
|
|
|
|
loop = asyncio.new_event_loop()
|
|
|
|
try:
|
|
|
|
asyncio.set_event_loop(loop)
|
2022-04-04 22:41:04 +02:00
|
|
|
loop.run_until_complete(
|
|
|
|
self._execute(
|
2023-01-24 23:56:28 +01:00
|
|
|
device_id=dev.device_id,
|
2022-04-04 22:41:04 +02:00
|
|
|
capability=capability,
|
|
|
|
command=command,
|
|
|
|
component_id=component_id,
|
|
|
|
args=args,
|
|
|
|
)
|
|
|
|
)
|
2021-03-05 02:23:28 +01:00
|
|
|
finally:
|
|
|
|
loop.stop()
|
|
|
|
|
2022-05-30 09:23:05 +02:00
|
|
|
@staticmethod
|
2023-02-03 02:20:20 +01:00
|
|
|
def _property_to_entity_name( # pylint: disable=redefined-builtin
|
2023-02-02 23:21:12 +01:00
|
|
|
property: str,
|
2023-02-03 02:20:20 +01:00
|
|
|
) -> str:
|
2023-01-26 22:10:02 +01:00
|
|
|
return ' '.join(
|
|
|
|
[
|
|
|
|
t[:1].upper() + t[1:]
|
|
|
|
for t in camel_case_to_snake_case(property).split('_')
|
|
|
|
]
|
|
|
|
)
|
|
|
|
|
|
|
|
@classmethod
|
2023-02-02 23:21:12 +01:00
|
|
|
def _to_entity( # pylint: disable=redefined-builtin
|
2023-01-26 22:10:02 +01:00
|
|
|
cls, device: DeviceEntity, property: str, entity_type: Type[Entity], **kwargs
|
2023-01-24 23:56:28 +01:00
|
|
|
) -> Entity:
|
2023-01-21 14:09:26 +01:00
|
|
|
return entity_type(
|
2023-01-24 23:56:28 +01:00
|
|
|
id=f'{device.device_id}:{property}',
|
2023-01-26 22:10:02 +01:00
|
|
|
name=cls._property_to_entity_name(property),
|
2023-01-21 14:09:26 +01:00
|
|
|
**kwargs,
|
|
|
|
)
|
2022-05-30 09:23:05 +02:00
|
|
|
|
2023-01-26 22:10:02 +01:00
|
|
|
@classmethod
|
|
|
|
def _get_status_attr_info(cls, device: DeviceEntity, mapper: DeviceMapper) -> dict:
|
|
|
|
status = device.status.attributes.get(mapper.attribute)
|
2023-01-24 23:56:28 +01:00
|
|
|
info = {}
|
|
|
|
if status:
|
2023-01-26 22:10:02 +01:00
|
|
|
info.update(
|
|
|
|
{
|
|
|
|
attr: getattr(status, attr, None)
|
|
|
|
for attr in ('unit', 'min', 'max')
|
|
|
|
if getattr(status, attr, None) is not None
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
supported_values = mapper.values
|
|
|
|
if isinstance(mapper.value_type, str):
|
|
|
|
# The list of supported values is actually contained on a
|
|
|
|
# device attribute
|
|
|
|
try:
|
|
|
|
supported_values = getattr(
|
|
|
|
device.status, mapper.value_type, mapper.values
|
|
|
|
)
|
|
|
|
except Exception:
|
|
|
|
pass
|
|
|
|
|
|
|
|
if supported_values:
|
|
|
|
info['values'] = mapper.values
|
2023-01-24 23:56:28 +01:00
|
|
|
|
|
|
|
return info
|
|
|
|
|
2023-01-26 22:10:02 +01:00
|
|
|
@staticmethod
|
|
|
|
def _merge_dicts(*dicts: dict) -> dict:
|
|
|
|
ret = {}
|
|
|
|
for d in dicts:
|
|
|
|
ret.update(d)
|
|
|
|
return ret
|
|
|
|
|
2023-01-21 14:09:26 +01:00
|
|
|
@classmethod
|
2023-01-24 23:56:28 +01:00
|
|
|
def _get_supported_entities(
|
|
|
|
cls,
|
2023-01-26 22:10:02 +01:00
|
|
|
device: DeviceEntity,
|
2023-01-24 23:56:28 +01:00
|
|
|
entity_type: Optional[Type[Entity]] = None,
|
|
|
|
entity_value_attr: str = 'value',
|
|
|
|
**default_entity_args,
|
|
|
|
) -> List[Entity]:
|
|
|
|
mappers = [
|
2023-01-26 22:10:02 +01:00
|
|
|
mapper
|
|
|
|
for mapper in device_mappers
|
|
|
|
if (entity_type is None or issubclass(mapper.entity_type, entity_type))
|
|
|
|
and mapper.capability in device.capabilities
|
2023-01-24 23:56:28 +01:00
|
|
|
]
|
|
|
|
|
|
|
|
return [
|
|
|
|
cls._to_entity(
|
|
|
|
device,
|
2023-01-26 22:10:02 +01:00
|
|
|
property=mapper.attribute,
|
|
|
|
entity_type=mapper.entity_type,
|
|
|
|
**cls._merge_dicts(
|
|
|
|
{entity_value_attr: mapper.get_value(device)},
|
|
|
|
default_entity_args,
|
|
|
|
mapper.entity_args,
|
|
|
|
cls._get_status_attr_info(device, mapper),
|
|
|
|
),
|
2023-01-24 23:56:28 +01:00
|
|
|
)
|
2023-01-26 22:10:02 +01:00
|
|
|
for mapper in mappers
|
2023-01-24 23:56:28 +01:00
|
|
|
]
|
|
|
|
|
|
|
|
@classmethod
|
2023-01-26 22:10:02 +01:00
|
|
|
def _get_lights(cls, device: DeviceEntity) -> Iterable[Light]:
|
2023-01-21 14:09:26 +01:00
|
|
|
if not (
|
2023-01-24 23:56:28 +01:00
|
|
|
{Capability.color_control, Capability.color_temperature}.intersection(
|
|
|
|
device.capabilities
|
2023-01-21 14:09:26 +01:00
|
|
|
)
|
|
|
|
):
|
|
|
|
return []
|
|
|
|
|
|
|
|
light_attrs = {}
|
2023-01-26 22:10:02 +01:00
|
|
|
status = device.status
|
|
|
|
|
2023-01-24 23:56:28 +01:00
|
|
|
if Capability.switch in device.capabilities:
|
2023-01-26 22:10:02 +01:00
|
|
|
light_attrs['on'] = status.switch
|
2023-01-24 23:56:28 +01:00
|
|
|
if Capability.switch_level in device.capabilities:
|
2023-01-26 22:10:02 +01:00
|
|
|
light_attrs['brightness'] = status.level
|
2023-01-21 14:09:26 +01:00
|
|
|
light_attrs['brightness_min'] = 0
|
|
|
|
light_attrs['brightness_max'] = 100
|
2023-01-24 23:56:28 +01:00
|
|
|
if Capability.color_temperature in device.capabilities:
|
2023-01-26 22:10:02 +01:00
|
|
|
light_attrs['temperature'] = status.color_temperature
|
2023-01-24 23:56:28 +01:00
|
|
|
light_attrs['temperature_min'] = 1
|
|
|
|
light_attrs['temperature_max'] = 30000
|
2023-01-26 22:10:02 +01:00
|
|
|
if getattr(status, 'hue', None) is not None:
|
|
|
|
light_attrs['hue'] = status.hue
|
2023-01-21 14:09:26 +01:00
|
|
|
light_attrs['hue_min'] = 0
|
|
|
|
light_attrs['hue_max'] = 100
|
2023-01-26 22:10:02 +01:00
|
|
|
if getattr(status, 'saturation', None) is not None:
|
|
|
|
light_attrs['saturation'] = status.saturation
|
2023-01-21 14:09:26 +01:00
|
|
|
light_attrs['saturation_min'] = 0
|
2023-01-22 01:01:47 +01:00
|
|
|
light_attrs['saturation_max'] = 100
|
2023-01-21 14:09:26 +01:00
|
|
|
|
2023-01-24 23:56:28 +01:00
|
|
|
return [cls._to_entity(device, 'light', Light, **light_attrs)]
|
2023-01-21 14:09:26 +01:00
|
|
|
|
|
|
|
@classmethod
|
2023-01-26 22:10:02 +01:00
|
|
|
def _get_switches(cls, device: DeviceEntity) -> Iterable[Switch]:
|
2023-01-24 23:56:28 +01:00
|
|
|
return cls._get_supported_entities(device, Switch, entity_value_attr='state')
|
2021-03-05 02:23:28 +01:00
|
|
|
|
2023-01-21 14:09:26 +01:00
|
|
|
@classmethod
|
2023-01-26 22:10:02 +01:00
|
|
|
def _get_enum_switches(cls, device: DeviceEntity) -> Iterable[Switch]:
|
|
|
|
return cls._get_supported_entities(device, EnumSwitch)
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def _get_dimmers(cls, device: DeviceEntity) -> Iterable[Dimmer]:
|
2023-01-24 23:56:28 +01:00
|
|
|
return cls._get_supported_entities(device, Dimmer, min=0, max=100)
|
2022-05-30 09:23:05 +02:00
|
|
|
|
2023-01-21 14:09:26 +01:00
|
|
|
@classmethod
|
|
|
|
def _get_sensors(cls, device) -> Iterable[Sensor]:
|
2023-01-24 23:56:28 +01:00
|
|
|
return cls._get_supported_entities(device, Sensor)
|
2023-01-22 01:01:47 +01:00
|
|
|
|
2023-01-21 14:09:26 +01:00
|
|
|
def transform_entities(self, entities):
|
|
|
|
compatible_entities = []
|
|
|
|
|
|
|
|
for entity in entities:
|
|
|
|
device_entities = [
|
|
|
|
*self._get_lights(entity),
|
|
|
|
*self._get_switches(entity),
|
|
|
|
*self._get_dimmers(entity),
|
|
|
|
*self._get_sensors(entity),
|
|
|
|
]
|
|
|
|
|
|
|
|
if device_entities:
|
|
|
|
parent = Device(
|
|
|
|
id=entity.device_id,
|
|
|
|
name=entity.label,
|
|
|
|
)
|
|
|
|
|
|
|
|
for child in device_entities:
|
|
|
|
child.parent = parent
|
|
|
|
|
|
|
|
device_entities.insert(0, parent)
|
|
|
|
|
|
|
|
compatible_entities += device_entities
|
2022-04-11 00:42:14 +02:00
|
|
|
|
2023-01-24 23:56:28 +01:00
|
|
|
self._entities_by_id.update({e.id: e for e in compatible_entities})
|
|
|
|
|
2022-04-11 00:42:14 +02:00
|
|
|
return super().transform_entities(compatible_entities) # type: ignore
|
|
|
|
|
2023-01-22 00:09:10 +01:00
|
|
|
async def _get_device_status(
|
|
|
|
self, api, device_id: str, publish_entities: bool
|
|
|
|
) -> dict:
|
2022-04-11 00:42:14 +02:00
|
|
|
device = await api.device(device_id)
|
2023-01-22 00:09:10 +01:00
|
|
|
assert device, f'No such device: {device_id}'
|
2022-04-11 00:42:14 +02:00
|
|
|
await device.status.refresh()
|
2023-01-22 00:09:10 +01:00
|
|
|
if publish_entities:
|
|
|
|
self.publish_entities([device]) # type: ignore
|
|
|
|
|
|
|
|
self._devices_by_id[device_id] = device
|
|
|
|
self._devices_by_name[device.label] = device
|
|
|
|
for i, dev in enumerate(self._devices):
|
|
|
|
if dev.device_id == device_id:
|
|
|
|
self._devices[i] = device
|
|
|
|
break
|
2022-04-04 22:41:04 +02:00
|
|
|
|
2021-03-05 02:23:28 +01:00
|
|
|
return {
|
|
|
|
'device_id': device_id,
|
|
|
|
'name': device.label,
|
|
|
|
**{
|
|
|
|
cap: getattr(device.status, cap)
|
|
|
|
for cap in device.capabilities
|
|
|
|
if hasattr(device.status, cap)
|
|
|
|
and not callable(getattr(device.status, cap))
|
2022-04-04 22:41:04 +02:00
|
|
|
},
|
2021-03-05 02:23:28 +01:00
|
|
|
}
|
|
|
|
|
2023-01-22 00:09:10 +01:00
|
|
|
async def _refresh_status(
|
|
|
|
self, devices: List[str], publish_entities: bool = True
|
|
|
|
) -> List[dict]:
|
2021-03-05 02:23:28 +01:00
|
|
|
device_ids = []
|
|
|
|
missing_device_ids = set()
|
|
|
|
|
|
|
|
def parse_device_id(device):
|
|
|
|
device_id = None
|
|
|
|
if device in self._devices_by_id:
|
|
|
|
device_id = device
|
|
|
|
device_ids.append(device_id)
|
|
|
|
elif device in self._devices_by_name:
|
|
|
|
device_id = self._devices_by_name[device].device_id
|
|
|
|
device_ids.append(device_id)
|
|
|
|
else:
|
|
|
|
missing_device_ids.add(device)
|
|
|
|
|
|
|
|
if device_id and device in missing_device_ids:
|
|
|
|
missing_device_ids.remove(device)
|
|
|
|
|
|
|
|
for dev in devices:
|
|
|
|
parse_device_id(dev)
|
|
|
|
|
|
|
|
# Fail if some devices haven't been found after refreshing
|
2022-04-04 22:41:04 +02:00
|
|
|
assert (
|
|
|
|
not missing_device_ids
|
2023-02-02 23:21:12 +01:00
|
|
|
), f'Could not find the following devices: {list(missing_device_ids)}'
|
2021-03-05 02:23:28 +01:00
|
|
|
|
|
|
|
async with aiohttp.ClientSession(timeout=self._timeout) as session:
|
2023-01-24 23:56:28 +01:00
|
|
|
api = SmartThings(session, self._access_token)
|
2021-03-05 02:23:28 +01:00
|
|
|
status_tasks = [
|
2023-01-22 00:09:10 +01:00
|
|
|
asyncio.ensure_future(
|
|
|
|
self._get_device_status(
|
|
|
|
api, device_id, publish_entities=publish_entities
|
|
|
|
)
|
|
|
|
)
|
2021-03-05 02:23:28 +01:00
|
|
|
for device_id in device_ids
|
|
|
|
]
|
|
|
|
|
|
|
|
return await asyncio.gather(*status_tasks)
|
|
|
|
|
|
|
|
@action
|
2023-02-02 23:21:12 +01:00
|
|
|
def status( # pylint: disable=arguments-differ
|
|
|
|
self, device: Optional[Union[str, List[str]]] = None, publish_entities=True, **_
|
2023-01-22 00:09:10 +01:00
|
|
|
) -> List[dict]:
|
2021-03-05 02:23:28 +01:00
|
|
|
"""
|
|
|
|
Refresh and return the status of one or more devices.
|
|
|
|
|
|
|
|
:param device: Device or list of devices to refresh (default: all)
|
|
|
|
:return: A list containing on entry per device, and each entry containing the current device state. Example:
|
|
|
|
|
|
|
|
.. code-block:: json
|
|
|
|
|
|
|
|
[
|
|
|
|
{
|
|
|
|
"device_id": "switch-1",
|
|
|
|
"name": "Fan",
|
|
|
|
"switch": false
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"device_id": "tv-1",
|
|
|
|
"name": "Samsung Smart TV",
|
|
|
|
"switch": true
|
|
|
|
}
|
|
|
|
]
|
|
|
|
|
|
|
|
"""
|
|
|
|
self.refresh_info()
|
|
|
|
|
|
|
|
if not device:
|
|
|
|
self.refresh_info()
|
2023-02-02 23:21:12 +01:00
|
|
|
devices = list(self._devices_by_id.keys())
|
2021-03-05 02:23:28 +01:00
|
|
|
elif isinstance(device, str):
|
|
|
|
devices = [device]
|
|
|
|
else:
|
|
|
|
devices = device
|
|
|
|
|
|
|
|
with self._refresh_lock:
|
|
|
|
loop = asyncio.new_event_loop()
|
|
|
|
try:
|
|
|
|
asyncio.set_event_loop(loop)
|
2023-01-22 00:09:10 +01:00
|
|
|
return loop.run_until_complete(
|
|
|
|
self._refresh_status(
|
|
|
|
list(devices), publish_entities=publish_entities
|
|
|
|
)
|
|
|
|
)
|
2021-03-05 02:23:28 +01:00
|
|
|
finally:
|
|
|
|
loop.stop()
|
|
|
|
|
2023-01-24 23:56:28 +01:00
|
|
|
def _set_switch(self, device: str, value: Optional[bool] = None):
|
2023-02-02 23:21:12 +01:00
|
|
|
(
|
|
|
|
device,
|
2023-02-03 02:20:20 +01:00
|
|
|
property, # pylint: disable=redefined-builtin
|
|
|
|
) = self._to_device_and_property(device)
|
2023-02-02 23:21:12 +01:00
|
|
|
|
2023-01-24 23:56:28 +01:00
|
|
|
if not property:
|
|
|
|
property = Attribute.switch
|
|
|
|
|
|
|
|
if value is None:
|
|
|
|
# Toggle case
|
|
|
|
dev = self._get_device(device)
|
2023-01-26 22:10:02 +01:00
|
|
|
if property == 'light':
|
|
|
|
property = 'switch'
|
|
|
|
else:
|
2023-02-02 23:21:12 +01:00
|
|
|
assert property, 'No property specified'
|
2023-01-26 22:10:02 +01:00
|
|
|
assert hasattr(
|
|
|
|
dev.status, property
|
|
|
|
), f'No such property on device "{dev.label}": "{property}"'
|
|
|
|
|
|
|
|
value = getattr(dev.status, property, None)
|
|
|
|
assert value is not None, (
|
|
|
|
f'Could not get the current value of "{property}" for the '
|
|
|
|
f'device "{dev.device_id}"'
|
|
|
|
)
|
2023-01-24 23:56:28 +01:00
|
|
|
|
2023-01-26 22:10:02 +01:00
|
|
|
value = not value # Toggle
|
2023-01-24 23:56:28 +01:00
|
|
|
device = dev.device_id
|
|
|
|
|
|
|
|
return self.set_value(device, property, value)
|
|
|
|
|
2021-03-05 02:23:28 +01:00
|
|
|
@action
|
2023-02-02 23:21:12 +01:00
|
|
|
def on(self, device: str, *_, **__): # pylint: disable=arguments-differ
|
2021-03-05 02:23:28 +01:00
|
|
|
"""
|
|
|
|
Turn on a device with ``switch`` capability.
|
|
|
|
|
|
|
|
:param device: Device name or ID.
|
|
|
|
"""
|
2023-01-24 23:56:28 +01:00
|
|
|
return self._set_switch(device, True)
|
2021-03-05 02:23:28 +01:00
|
|
|
|
|
|
|
@action
|
2023-02-02 23:21:12 +01:00
|
|
|
def off(self, device: str, *_, **__): # pylint: disable=arguments-differ
|
2021-03-05 02:23:28 +01:00
|
|
|
"""
|
|
|
|
Turn off a device with ``switch`` capability.
|
|
|
|
|
|
|
|
:param device: Device name or ID.
|
|
|
|
"""
|
2023-01-24 23:56:28 +01:00
|
|
|
return self._set_switch(device, False)
|
2021-03-05 02:23:28 +01:00
|
|
|
|
|
|
|
@action
|
2023-02-02 23:21:12 +01:00
|
|
|
def toggle(self, device: str, *_, **__): # pylint: disable=arguments-differ
|
2021-03-05 02:23:28 +01:00
|
|
|
"""
|
|
|
|
Toggle a device with ``switch`` capability.
|
|
|
|
|
|
|
|
:param device: Device name or ID.
|
2021-03-05 21:29:32 +01:00
|
|
|
:return: Device status
|
2021-03-05 02:23:28 +01:00
|
|
|
"""
|
2023-01-24 23:56:28 +01:00
|
|
|
return self._set_switch(device)
|
2021-03-05 02:23:28 +01:00
|
|
|
|
2022-05-30 09:23:05 +02:00
|
|
|
@action
|
|
|
|
def set_level(self, device: str, level: int, **kwargs):
|
|
|
|
"""
|
|
|
|
Set the level of a device with ``switchLevel`` capabilities (e.g. the
|
|
|
|
brightness of a lightbulb or the speed of a fan).
|
|
|
|
|
|
|
|
:param device: Device ID or name.
|
|
|
|
:param level: Level, usually a percentage value between 0 and 1.
|
|
|
|
:param kwarsg: Extra arguments that should be passed to :meth:`.execute`.
|
|
|
|
"""
|
2023-01-24 23:56:28 +01:00
|
|
|
return self.set_value(device, Capability.switch_level, level, **kwargs)
|
|
|
|
|
2023-02-02 23:21:12 +01:00
|
|
|
def _set_value( # pylint: disable=redefined-builtin
|
2023-01-24 23:56:28 +01:00
|
|
|
self, device: str, property: Optional[str] = None, data=None, **kwargs
|
|
|
|
):
|
|
|
|
if not property:
|
|
|
|
device, property = self._to_device_and_property(device)
|
|
|
|
|
|
|
|
assert property, 'No property name specified'
|
|
|
|
assert data is not None, 'No value specified'
|
|
|
|
entity_id = f'{device}:{property}'
|
|
|
|
entity = self._entities_by_id.get(entity_id)
|
|
|
|
assert entity, f'No such entity ID: {entity_id}'
|
|
|
|
|
|
|
|
mapper = next(
|
|
|
|
iter([m for m in device_mappers if m.attribute == property]), None
|
|
|
|
)
|
|
|
|
|
|
|
|
assert mapper, f'No mappers found to set {property}={data} on device "{device}"'
|
|
|
|
assert (
|
|
|
|
mapper.set_command
|
|
|
|
), f'The property "{property}" on the device "{device}" cannot be set'
|
2023-01-26 22:10:02 +01:00
|
|
|
|
2023-01-24 23:56:28 +01:00
|
|
|
command = (
|
|
|
|
mapper.set_command(data)
|
|
|
|
if callable(mapper.set_command)
|
|
|
|
else mapper.set_command
|
|
|
|
)
|
|
|
|
|
|
|
|
self.execute(
|
|
|
|
device,
|
|
|
|
mapper.capability,
|
|
|
|
command,
|
2023-02-02 23:21:12 +01:00
|
|
|
args=mapper.set_value_args(data), # type: ignore
|
2023-01-24 23:56:28 +01:00
|
|
|
**kwargs,
|
|
|
|
)
|
|
|
|
|
|
|
|
return self.status(device)
|
2022-05-30 09:23:05 +02:00
|
|
|
|
2023-01-21 14:09:26 +01:00
|
|
|
@action
|
2023-02-02 23:21:12 +01:00
|
|
|
def set_value( # pylint: disable=arguments-differ,redefined-builtin
|
2023-01-21 14:09:26 +01:00
|
|
|
self, device: str, property: Optional[str] = None, data=None, **kwargs
|
|
|
|
):
|
|
|
|
"""
|
|
|
|
Set the value of a device. It is compatible with the generic
|
|
|
|
``set_value`` method required by entities.
|
|
|
|
|
2023-01-24 23:56:28 +01:00
|
|
|
:param device: Device ID or device+property name string in the format
|
|
|
|
``device_id:property``.
|
2023-01-21 14:09:26 +01:00
|
|
|
:param property: Name of the property to be set. If not specified here
|
|
|
|
then it should be specified on the ``device`` level.
|
|
|
|
:param data: Value to be set.
|
|
|
|
"""
|
2023-01-24 23:56:28 +01:00
|
|
|
try:
|
|
|
|
return self._set_value(device, property, data, **kwargs)
|
|
|
|
except Exception as e:
|
|
|
|
self.logger.exception(e)
|
2023-02-02 23:21:12 +01:00
|
|
|
raise AssertionError(e) from e
|
2023-01-21 14:09:26 +01:00
|
|
|
|
2022-05-30 09:23:05 +02:00
|
|
|
@action
|
2023-02-02 23:21:12 +01:00
|
|
|
def set_lights( # pylint: disable=arguments-differ,redefined-builtin
|
2022-05-30 09:23:05 +02:00
|
|
|
self,
|
|
|
|
lights: Iterable[str],
|
|
|
|
on: Optional[bool] = None,
|
|
|
|
brightness: Optional[int] = None,
|
|
|
|
hue: Optional[int] = None,
|
|
|
|
saturation: Optional[int] = None,
|
|
|
|
hex: Optional[str] = None,
|
|
|
|
temperature: Optional[int] = None,
|
|
|
|
**_,
|
|
|
|
):
|
|
|
|
err = None
|
|
|
|
|
|
|
|
with self._execute_lock:
|
|
|
|
for light in lights:
|
|
|
|
try:
|
|
|
|
if on is not None:
|
2023-01-24 23:56:28 +01:00
|
|
|
self.execute(
|
|
|
|
light, Capability.switch, Command.on if on else Command.off
|
|
|
|
)
|
2022-05-30 09:23:05 +02:00
|
|
|
if brightness is not None:
|
|
|
|
self.execute(
|
2023-01-24 23:56:28 +01:00
|
|
|
light,
|
|
|
|
Capability.switch_level,
|
|
|
|
Command.set_level,
|
|
|
|
args=[brightness],
|
2022-05-30 09:23:05 +02:00
|
|
|
)
|
|
|
|
if hue is not None:
|
2023-01-24 23:56:28 +01:00
|
|
|
self.execute(
|
|
|
|
light, Capability.color_control, Command.set_hue, args=[hue]
|
|
|
|
)
|
2022-05-30 09:23:05 +02:00
|
|
|
if saturation is not None:
|
|
|
|
self.execute(
|
2023-01-24 23:56:28 +01:00
|
|
|
light,
|
|
|
|
Capability.color_control,
|
|
|
|
Command.set_saturation,
|
|
|
|
args=[saturation],
|
2022-05-30 09:23:05 +02:00
|
|
|
)
|
|
|
|
if temperature is not None:
|
|
|
|
self.execute(
|
|
|
|
light,
|
2023-01-24 23:56:28 +01:00
|
|
|
Capability.color_temperature,
|
|
|
|
Command.set_color_temperature,
|
2022-05-30 09:23:05 +02:00
|
|
|
args=[temperature],
|
|
|
|
)
|
|
|
|
if hex is not None:
|
2023-01-24 23:56:28 +01:00
|
|
|
self.execute(
|
|
|
|
light,
|
|
|
|
Capability.color_control,
|
|
|
|
Command.set_color,
|
|
|
|
args=[hex],
|
|
|
|
)
|
2022-05-30 09:23:05 +02:00
|
|
|
except Exception as e:
|
|
|
|
self.logger.error('Could not set attributes on %s: %s', light, e)
|
|
|
|
err = e
|
|
|
|
|
|
|
|
if err:
|
|
|
|
raise err
|
|
|
|
|
2023-01-22 00:09:10 +01:00
|
|
|
@staticmethod
|
2023-01-24 23:56:28 +01:00
|
|
|
def _device_status_to_dict(status: DeviceStatus) -> dict:
|
2023-01-22 00:09:10 +01:00
|
|
|
status_dict = {}
|
2023-01-24 23:56:28 +01:00
|
|
|
for attr in status.attributes:
|
2023-01-22 00:09:10 +01:00
|
|
|
attr = camel_case_to_snake_case(attr)
|
2023-01-24 23:56:28 +01:00
|
|
|
try:
|
|
|
|
if hasattr(status, attr):
|
|
|
|
status_dict[attr] = getattr(status, attr)
|
|
|
|
except Exception:
|
|
|
|
# Ignore exceptions if retrieving status attributes that don't
|
|
|
|
# apply to this device
|
|
|
|
continue
|
2023-01-22 00:09:10 +01:00
|
|
|
|
|
|
|
return status_dict
|
|
|
|
|
|
|
|
def _get_devices_status_dict(self) -> Dict[str, dict]:
|
2023-01-24 23:56:28 +01:00
|
|
|
return dict(
|
|
|
|
filter(
|
|
|
|
lambda d: bool(d[1]),
|
|
|
|
[
|
|
|
|
(device_id, self._device_status_to_dict(device.status))
|
|
|
|
for device_id, device in self._devices_by_id.items()
|
|
|
|
],
|
|
|
|
)
|
|
|
|
)
|
2023-01-22 00:09:10 +01:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _has_status_changed(status: dict, new_status: dict) -> bool:
|
|
|
|
if not status and new_status:
|
|
|
|
return True
|
|
|
|
|
|
|
|
for attr, value in status.items():
|
|
|
|
if attr in new_status:
|
|
|
|
new_value = new_status[attr]
|
|
|
|
if value != new_value:
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
def main(self):
|
2023-01-24 23:56:28 +01:00
|
|
|
def refresh_status_safe():
|
|
|
|
try:
|
|
|
|
return self.status(publish_entities=False)
|
|
|
|
except Exception as e:
|
|
|
|
self.logger.exception(e)
|
2023-02-02 23:21:12 +01:00
|
|
|
self.logger.error('Could not refresh the status: %s', e)
|
2023-01-24 23:56:28 +01:00
|
|
|
self.wait_stop(3 * (self.poll_interval or 5))
|
|
|
|
|
2023-01-22 00:09:10 +01:00
|
|
|
while not self.should_stop():
|
|
|
|
updated_devices = {}
|
|
|
|
devices = self._get_devices_status_dict()
|
2023-01-24 23:56:28 +01:00
|
|
|
status = refresh_status_safe()
|
|
|
|
if not status:
|
|
|
|
continue
|
|
|
|
|
2023-01-22 00:09:10 +01:00
|
|
|
new_devices = self._get_devices_status_dict()
|
|
|
|
|
|
|
|
updated_devices = {
|
|
|
|
device_id: self._devices_by_id[device_id]
|
|
|
|
for device_id, new_status in new_devices.items()
|
|
|
|
if self._has_status_changed(devices.get(device_id, {}), new_status)
|
|
|
|
}
|
|
|
|
|
|
|
|
self.publish_entities(updated_devices.values()) # type: ignore
|
2023-01-24 23:56:28 +01:00
|
|
|
devices.update(new_devices)
|
2023-01-22 00:09:10 +01:00
|
|
|
self.wait_stop(self.poll_interval)
|
2023-01-24 23:56:28 +01:00
|
|
|
refresh_status_safe()
|
2023-01-22 00:09:10 +01:00
|
|
|
|
2021-03-05 02:23:28 +01:00
|
|
|
|
|
|
|
# vim:sw=4:ts=4:et:
|