Proper support for light entities on smartthings

This commit is contained in:
Fabio Manganiello 2022-05-30 09:23:05 +02:00
parent 0689e05e96
commit 1df71cb54a
Signed by: blacklight
GPG key ID: D90FBA7F76362774
4 changed files with 166 additions and 36 deletions

View file

@ -2,7 +2,8 @@
<div class="entity light-container">
<div class="head" :class="{expanded: expanded}">
<div class="col-1 icon">
<EntityIcon :icon="icon" :loading="loading" :error="error" />
<EntityIcon :icon="icon" :hasColorFill="true"
:loading="loading" :error="error" />
</div>
<div class="col-s-8 col-m-9 label">
@ -10,13 +11,13 @@
</div>
<div class="col-s-3 col-m-2 buttons pull-right">
<ToggleSwitch :value="value.on" @input="toggle"
@click.stop :disabled="loading || value.is_read_only" />
<button @click.stop="expanded = !expanded">
<i class="fas"
:class="{'fa-angle-up': expanded, 'fa-angle-down': !expanded}" />
</button>
<ToggleSwitch :value="value.on" @input="toggle"
@click.stop :disabled="loading || value.is_read_only" />
</div>
</div>
@ -84,14 +85,17 @@ export default {
computed: {
rgbColor() {
if (
!this.colorConverter || this.value.hue == null ||
(this.value.x == null && this.value.y == null)
)
return
if (this.value.meta?.icon?.color)
return this.value.meta.icon.color
if (
!this.colorConverter || (
this.value.hue == null &&
(this.value.x == null || this.value.y == null)
)
)
return
if (this.value.x && this.value.y)
return this.colorConverter.xyToRgb(
this.value.x,
@ -184,6 +188,14 @@ export default {
@import "common";
.light-container {
.head {
.buttons {
button {
margin-right: 0.5em;
}
}
}
.body {
.row {
display: flex;

View file

@ -49,7 +49,7 @@ class Entity(Base):
UniqueConstraint(external_id, plugin)
__table_args__ = (Index(name, plugin),)
__table_args__ = (Index(name, plugin), Index(name, type, plugin))
__mapper_args__ = {
'polymorphic_identity': __tablename__,

View file

@ -188,7 +188,9 @@ class EntitiesEngine(Thread):
)
if entity.external_id is not None
else and_(
Entity.name == entity.name, Entity.plugin == entity.plugin
Entity.name == entity.name,
Entity.type == entity.type,
Entity.plugin == entity.plugin,
)
for entity in entities
]
@ -246,6 +248,10 @@ class EntitiesEngine(Thread):
def _process_entities(self, *entities: Entity):
with self._get_db().get_session() as session:
# Ensure that the internal IDs are set to null before the merge
for e in entities:
e.id = None # type: ignore
existing_entities = self._get_if_exist(session, entities)
entities = self._merge_entities(entities, existing_entities) # type: ignore
session.add_all(entities)

View file

@ -2,13 +2,17 @@ import asyncio
import aiohttp
from threading import RLock
from typing import Optional, Dict, List, Union
from typing import Optional, Dict, List, Union, Iterable
from platypush.entities import manages
from platypush.entities.lights import Light
from platypush.entities.switches import Switch
from platypush.plugins import action
from platypush.plugins.switch import SwitchPlugin
from platypush.plugins.switch import Plugin
class SmartthingsPlugin(SwitchPlugin):
@manages(Switch, Light)
class SmartthingsPlugin(Plugin):
"""
Plugin to interact with devices and locations registered to a Samsung SmartThings account.
@ -43,16 +47,12 @@ class SmartthingsPlugin(SwitchPlugin):
async def _refresh_locations(self, api):
self._locations = await api.locations()
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}
async def _refresh_devices(self, api):
self._devices = await api.devices()
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}
async def _refresh_rooms(self, api, location_id: str):
@ -300,12 +300,24 @@ class SmartthingsPlugin(SwitchPlugin):
return self._location_to_dict(location)
def _get_device(self, device: str):
if device not in self._devices_by_id or device not in self._devices_by_name:
return self._get_devices(device)[0]
def _get_devices(self, *devices: str):
def get_found_and_missing_devs():
found_devs = [
self._devices_by_id.get(d, self._devices_by_name.get(d))
for d in devices
]
missing_devs = [d for i, d in enumerate(devices) if not found_devs[i]]
return found_devs, missing_devs
devs, missing_devs = get_found_and_missing_devs()
if missing_devs:
self.refresh_info()
device = self._devices_by_id.get(device, self._devices_by_name.get(device))
assert device, 'Device {} not found'.format(device)
return device
devs, missing_devs = get_found_and_missing_devs()
assert not missing_devs, f'Devices not found: {missing_devs}'
return devs
@action
def get_device(self, device: str) -> dict:
@ -413,22 +425,68 @@ class SmartthingsPlugin(SwitchPlugin):
finally:
loop.stop()
@staticmethod
def _is_light(device):
if isinstance(device, dict):
capabilities = device.get('capabilities', [])
else:
capabilities = device.capabilities
return 'colorControl' in capabilities or 'colorTemperature' in capabilities
def transform_entities(self, entities):
from platypush.entities.switches import Switch
compatible_entities = []
for device in entities:
if 'switch' in device.capabilities:
data = {
'location_id': getattr(device, 'location_id', None),
'room_id': getattr(device, 'room_id', None),
}
if self._is_light(device):
light_attrs = {
'id': device.device_id,
'name': device.label,
'data': data,
}
if 'switch' in device.capabilities:
light_attrs['on'] = device.status.switch
if getattr(device.status, 'level', None) is not None:
light_attrs['brightness'] = device.status.level
light_attrs['brightness_min'] = 0
light_attrs['brightness_max'] = 100
if 'colorTemperature' in device.capabilities:
# Color temperature range on SmartThings is expressed in Kelvin
light_attrs['temperature_min'] = 2000
light_attrs['temperature_max'] = 6500
if (
device.status.color_temperature
>= light_attrs['temperature_min']
):
light_attrs['temperature'] = (
light_attrs['temperature_max']
- light_attrs['temperature_min']
) / 2
if getattr(device.status, 'hue', None) is not None:
light_attrs['hue'] = device.status.hue
light_attrs['hue_min'] = 0
light_attrs['hue_max'] = 100
if getattr(device.status, 'saturation', None) is not None:
light_attrs['saturation'] = device.status.saturation
light_attrs['saturation_min'] = 0
light_attrs['saturation_max'] = 80
compatible_entities.append(Light(**light_attrs))
elif 'switch' in device.capabilities:
compatible_entities.append(
Switch(
id=device.device_id,
name=device.label,
state=device.status.switch,
data={
'location_id': getattr(device, 'location_id', None),
'room_id': getattr(device, 'room_id', None),
},
data=data,
)
)
@ -582,17 +640,15 @@ class SmartthingsPlugin(SwitchPlugin):
assert ret, 'The command switch={state} failed on device {device}'.format(
state=state, device=dev.label
)
return not dev.status.switch
with self._refresh_lock:
loop = asyncio.new_event_loop()
state = loop.run_until_complete(_toggle())
device.status.switch = state
self.publish_entities([device]) # type: ignore
loop.run_until_complete(_toggle())
device = loop.run_until_complete(self._refresh_status([device_id]))[0] # type: ignore
return {
'id': device_id,
'name': device.label,
'on': state,
'name': device['name'],
'on': device['switch'],
}
@property
@ -617,8 +673,7 @@ class SmartthingsPlugin(SwitchPlugin):
]
"""
# noinspection PyUnresolvedReferences
devices = self.status().output
devices = self.status().output # type: ignore
return [
{
'name': device['name'],
@ -626,8 +681,65 @@ class SmartthingsPlugin(SwitchPlugin):
'on': device['switch'],
}
for device in devices
if 'switch' in device
if 'switch' in device and not self._is_light(device)
]
@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`.
"""
self.execute(device, 'switchLevel', 'setLevel', args=[int(level)], **kwargs)
@action
def set_lights(
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:
self.execute(light, 'switch', 'on' if on else 'off')
if brightness is not None:
self.execute(
light, 'switchLevel', 'setLevel', args=[brightness]
)
if hue is not None:
self.execute(light, 'colorControl', 'setHue', args=[hue])
if saturation is not None:
self.execute(
light, 'colorControl', 'setSaturation', args=[saturation]
)
if temperature is not None:
self.execute(
light,
'colorTemperature',
'setColorTemperature',
args=[temperature],
)
if hex is not None:
self.execute(light, 'colorControl', 'setColor', args=[hex])
except Exception as e:
self.logger.error('Could not set attributes on %s: %s', light, e)
err = e
if err:
raise err
# vim:sw=4:ts=4:et: