2023-02-03 02:20:20 +01:00
|
|
|
from typing import (
|
|
|
|
Collection,
|
|
|
|
Dict,
|
|
|
|
List,
|
|
|
|
Mapping,
|
|
|
|
Optional,
|
|
|
|
Union,
|
|
|
|
)
|
2019-12-17 10:56:00 +01:00
|
|
|
|
2022-04-05 23:22:54 +02:00
|
|
|
from pyHS100 import (
|
|
|
|
SmartDevice,
|
|
|
|
SmartPlug,
|
|
|
|
SmartBulb,
|
|
|
|
SmartStrip,
|
|
|
|
Discover,
|
|
|
|
SmartDeviceException,
|
|
|
|
)
|
|
|
|
|
2023-02-03 02:20:20 +01:00
|
|
|
from platypush.entities import Entity, SwitchEntityManager
|
|
|
|
from platypush.plugins import RunnablePlugin, action
|
2018-06-26 19:10:53 +02:00
|
|
|
|
|
|
|
|
2023-02-03 02:20:20 +01:00
|
|
|
class SwitchTplinkPlugin(RunnablePlugin, SwitchEntityManager):
|
2018-06-26 19:10:53 +02:00
|
|
|
"""
|
|
|
|
Plugin to interact with TP-Link smart switches/plugs like the HS100
|
|
|
|
(https://www.tp-link.com/us/products/details/cat-5516_HS100.html).
|
|
|
|
|
|
|
|
Requires:
|
|
|
|
|
|
|
|
* **pyHS100** (``pip install pyHS100``)
|
2020-01-05 00:46:46 +01:00
|
|
|
|
2018-06-26 19:10:53 +02:00
|
|
|
"""
|
|
|
|
|
2023-02-03 02:20:20 +01:00
|
|
|
_ip_to_dev: Dict[str, SmartDevice] = {}
|
|
|
|
_alias_to_dev: Dict[str, SmartDevice] = {}
|
2018-06-26 22:59:33 +02:00
|
|
|
|
2022-04-04 16:50:17 +02:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
plugs: Optional[Union[Mapping[str, str], List[str]]] = None,
|
|
|
|
bulbs: Optional[Union[Mapping[str, str], List[str]]] = None,
|
2022-04-05 23:22:54 +02:00
|
|
|
strips: Optional[Union[Mapping[str, str], List[str]]] = None,
|
2023-02-03 02:20:20 +01:00
|
|
|
**kwargs,
|
2022-04-04 16:50:17 +02:00
|
|
|
):
|
2019-12-17 10:56:00 +01:00
|
|
|
"""
|
|
|
|
:param plugs: Optional list of IP addresses or name->address mapping if you have a static list of
|
|
|
|
TpLink plugs and you want to save on the scan time.
|
|
|
|
:param bulbs: Optional list of IP addresses or name->address mapping if you have a static list of
|
|
|
|
TpLink bulbs and you want to save on the scan time.
|
|
|
|
:param strips: Optional list of IP addresses or name->address mapping if you have a static list of
|
|
|
|
TpLink strips and you want to save on the scan time.
|
|
|
|
"""
|
2019-07-02 12:02:28 +02:00
|
|
|
super().__init__(**kwargs)
|
2018-06-26 22:59:33 +02:00
|
|
|
self._ip_to_dev = {}
|
|
|
|
self._alias_to_dev = {}
|
2019-12-17 10:56:00 +01:00
|
|
|
self._static_devices = {}
|
|
|
|
|
|
|
|
if isinstance(plugs, list):
|
|
|
|
plugs = {addr: addr for addr in plugs}
|
|
|
|
if isinstance(bulbs, list):
|
|
|
|
bulbs = {addr: addr for addr in bulbs}
|
|
|
|
if isinstance(strips, list):
|
|
|
|
strips = {addr: addr for addr in strips}
|
2018-06-26 22:59:33 +02:00
|
|
|
|
2019-12-17 10:56:00 +01:00
|
|
|
for name, addr in (plugs or {}).items():
|
|
|
|
self._static_devices[addr] = {
|
|
|
|
'name': name,
|
|
|
|
'type': SmartPlug,
|
|
|
|
}
|
|
|
|
|
|
|
|
for name, addr in (bulbs or {}).items():
|
|
|
|
self._static_devices[addr] = {
|
|
|
|
'name': name,
|
|
|
|
'type': SmartBulb,
|
|
|
|
}
|
|
|
|
|
|
|
|
for name, addr in (strips or {}).items():
|
|
|
|
self._static_devices[addr] = {
|
|
|
|
'name': name,
|
|
|
|
'type': SmartStrip,
|
|
|
|
}
|
|
|
|
|
|
|
|
self._update_devices()
|
|
|
|
|
2023-02-03 02:20:20 +01:00
|
|
|
def _update_devices(
|
|
|
|
self,
|
|
|
|
devices: Optional[Mapping[str, SmartDevice]] = None,
|
|
|
|
publish_entities: bool = True,
|
|
|
|
):
|
2019-12-17 10:56:00 +01:00
|
|
|
for (addr, info) in self._static_devices.items():
|
2020-06-08 19:43:08 +02:00
|
|
|
try:
|
|
|
|
dev = info['type'](addr)
|
|
|
|
self._alias_to_dev[info.get('name', dev.alias)] = dev
|
|
|
|
self._ip_to_dev[addr] = dev
|
|
|
|
except SmartDeviceException as e:
|
2023-02-03 02:20:20 +01:00
|
|
|
self.logger.warning('Could not communicate with device %s: %s', addr, e)
|
2019-12-17 10:56:00 +01:00
|
|
|
|
|
|
|
for (ip, dev) in (devices or {}).items():
|
2018-06-26 22:59:33 +02:00
|
|
|
self._ip_to_dev[ip] = dev
|
|
|
|
self._alias_to_dev[dev.alias] = dev
|
|
|
|
|
2023-02-03 02:20:20 +01:00
|
|
|
if devices and publish_entities:
|
|
|
|
self.publish_entities(devices.values())
|
2022-04-04 16:50:17 +02:00
|
|
|
|
2023-02-03 02:20:20 +01:00
|
|
|
def transform_entities(self, entities: Collection[SmartDevice]):
|
2022-04-04 16:50:17 +02:00
|
|
|
from platypush.entities.switches import Switch
|
2022-04-05 23:22:54 +02:00
|
|
|
|
2023-02-03 02:20:20 +01:00
|
|
|
return super().transform_entities(
|
2022-04-05 23:22:54 +02:00
|
|
|
[
|
|
|
|
Switch(
|
|
|
|
id=dev.host,
|
|
|
|
name=dev.alias,
|
|
|
|
state=dev.is_on,
|
|
|
|
data={
|
|
|
|
'current_consumption': dev.current_consumption(),
|
|
|
|
'ip': dev.host,
|
|
|
|
'host': dev.host,
|
|
|
|
'hw_info': dev.hw_info,
|
|
|
|
},
|
|
|
|
)
|
2023-02-03 02:20:20 +01:00
|
|
|
for dev in (entities or [])
|
2022-04-05 23:22:54 +02:00
|
|
|
]
|
|
|
|
)
|
2022-04-04 16:50:17 +02:00
|
|
|
|
2023-02-03 02:20:20 +01:00
|
|
|
def _scan(self, publish_entities: bool = True) -> Dict[str, SmartDevice]:
|
2019-12-17 10:56:00 +01:00
|
|
|
devices = Discover.discover()
|
2023-02-03 02:20:20 +01:00
|
|
|
self._update_devices(devices, publish_entities=publish_entities)
|
2018-06-26 22:59:33 +02:00
|
|
|
return devices
|
|
|
|
|
|
|
|
def _get_device(self, device, use_cache=True):
|
|
|
|
if not use_cache:
|
|
|
|
self._scan()
|
|
|
|
|
2022-04-05 23:22:54 +02:00
|
|
|
if isinstance(device, Entity):
|
|
|
|
device = device.external_id or device.name
|
|
|
|
|
2018-06-26 22:59:33 +02:00
|
|
|
if device in self._ip_to_dev:
|
|
|
|
return self._ip_to_dev[device]
|
|
|
|
|
|
|
|
if device in self._alias_to_dev:
|
|
|
|
return self._alias_to_dev[device]
|
|
|
|
|
|
|
|
if use_cache:
|
|
|
|
return self._get_device(device, use_cache=False)
|
2023-02-03 02:20:20 +01:00
|
|
|
raise RuntimeError(f'Device {device} not found')
|
2018-06-26 22:59:33 +02:00
|
|
|
|
2022-04-04 16:50:17 +02:00
|
|
|
def _set(self, device: SmartDevice, state: bool):
|
|
|
|
action_name = 'turn_on' if state else 'turn_off'
|
2023-02-03 02:20:20 +01:00
|
|
|
act = getattr(device, action_name, None)
|
|
|
|
assert act, (
|
|
|
|
f'No such action available on the device "{device.alias}": '
|
|
|
|
f'"{action_name}"'
|
|
|
|
)
|
|
|
|
act()
|
|
|
|
self.publish_entities([device])
|
2022-04-04 16:50:17 +02:00
|
|
|
return self._serialize(device)
|
|
|
|
|
2018-07-06 02:08:38 +02:00
|
|
|
@action
|
2023-02-03 02:20:20 +01:00
|
|
|
def on(self, device, **_): # pylint: disable=arguments-differ
|
2018-06-26 19:10:53 +02:00
|
|
|
"""
|
|
|
|
Turn on a device
|
|
|
|
|
2018-06-26 22:59:33 +02:00
|
|
|
:param device: Device IP, hostname or alias
|
2018-06-26 19:10:53 +02:00
|
|
|
:type device: str
|
|
|
|
"""
|
|
|
|
|
2018-06-26 22:59:33 +02:00
|
|
|
device = self._get_device(device)
|
2022-04-04 16:50:17 +02:00
|
|
|
return self._set(device, True)
|
2018-06-26 19:10:53 +02:00
|
|
|
|
2018-07-06 02:08:38 +02:00
|
|
|
@action
|
2023-02-03 02:20:20 +01:00
|
|
|
def off(self, device, **_): # pylint: disable=arguments-differ
|
2018-06-26 19:10:53 +02:00
|
|
|
"""
|
|
|
|
Turn off a device
|
|
|
|
|
2018-06-26 22:59:33 +02:00
|
|
|
:param device: Device IP, hostname or alias
|
2018-06-26 19:10:53 +02:00
|
|
|
:type device: str
|
|
|
|
"""
|
|
|
|
|
2018-06-26 22:59:33 +02:00
|
|
|
device = self._get_device(device)
|
2022-04-04 16:50:17 +02:00
|
|
|
return self._set(device, False)
|
2018-06-26 19:10:53 +02:00
|
|
|
|
2018-07-06 02:08:38 +02:00
|
|
|
@action
|
2023-02-03 02:20:20 +01:00
|
|
|
def toggle(self, device, **_): # pylint: disable=arguments-differ
|
2018-06-26 19:10:53 +02:00
|
|
|
"""
|
|
|
|
Toggle the state of a device (on/off)
|
|
|
|
|
2018-06-26 22:59:33 +02:00
|
|
|
:param device: Device IP, hostname or alias
|
2018-06-26 19:10:53 +02:00
|
|
|
:type device: str
|
|
|
|
"""
|
|
|
|
|
2019-07-02 12:02:28 +02:00
|
|
|
device = self._get_device(device)
|
2022-04-04 16:50:17 +02:00
|
|
|
return self._set(device, not device.is_on)
|
2018-06-26 19:10:53 +02:00
|
|
|
|
2022-04-04 16:50:17 +02:00
|
|
|
@staticmethod
|
|
|
|
def _serialize(device: SmartDevice) -> dict:
|
2019-07-02 12:02:28 +02:00
|
|
|
return {
|
|
|
|
'current_consumption': device.current_consumption(),
|
|
|
|
'id': device.host,
|
|
|
|
'ip': device.host,
|
|
|
|
'host': device.host,
|
|
|
|
'hw_info': device.hw_info,
|
|
|
|
'name': device.alias,
|
|
|
|
'on': device.is_on,
|
|
|
|
}
|
|
|
|
|
2023-02-03 02:20:20 +01:00
|
|
|
@action
|
|
|
|
def status(self, *_, **__) -> List[dict]:
|
|
|
|
"""
|
|
|
|
Retrieve the current status of the devices. Return format:
|
|
|
|
|
|
|
|
.. code-block:: json
|
|
|
|
|
|
|
|
[
|
|
|
|
{
|
|
|
|
"current_consumption": 0.5,
|
|
|
|
"id": "192.168.1.123",
|
|
|
|
"ip": "192.168.1.123",
|
|
|
|
"host": "192.168.1.123",
|
|
|
|
"hw_info": "00:11:22:33:44:55",
|
|
|
|
"name": "My Switch",
|
|
|
|
"on": true,
|
|
|
|
}
|
|
|
|
]
|
|
|
|
|
|
|
|
"""
|
2022-04-05 23:22:54 +02:00
|
|
|
return [self._serialize(dev) for dev in self._scan().values()]
|
2018-06-26 19:10:53 +02:00
|
|
|
|
2023-02-03 02:20:20 +01:00
|
|
|
def main(self):
|
|
|
|
devices = {ip: self._serialize(dev) for ip, dev in self._ip_to_dev}
|
|
|
|
|
|
|
|
while not self.should_stop():
|
|
|
|
new_devices = self._scan(publish_entities=False)
|
|
|
|
new_serialized_devices = {
|
|
|
|
ip: self._serialize(dev) for ip, dev in new_devices.items()
|
|
|
|
}
|
|
|
|
|
|
|
|
updated_devices = {
|
|
|
|
ip: new_devices[ip]
|
|
|
|
for ip, dev in new_serialized_devices.items()
|
|
|
|
if any(v != devices.get(ip, {}).get(k) for k, v in dev.items())
|
|
|
|
}
|
|
|
|
|
|
|
|
if updated_devices:
|
|
|
|
self.publish_entities(updated_devices.values())
|
|
|
|
|
|
|
|
devices = new_serialized_devices
|
|
|
|
self.wait_stop(self.poll_interval)
|
|
|
|
|
2018-06-26 19:10:53 +02:00
|
|
|
|
|
|
|
# vim:sw=4:ts=4:et:
|