diff --git a/platypush/backend/http/static/css/source/webpanel/plugins/switches/index.scss b/platypush/backend/http/static/css/source/webpanel/plugins/switches/index.scss new file mode 100644 index 000000000..55a87a2f8 --- /dev/null +++ b/platypush/backend/http/static/css/source/webpanel/plugins/switches/index.scss @@ -0,0 +1,36 @@ +@import 'common/vars'; + +$head-bg: #e8e8e8; + +.switches-root { + .switch-root { + .head { + padding: 1rem .5rem; + background: $head-bg; + border-top: $default-border-2; + border-bottom: $default-border-2; + text-transform: uppercase; + } + + .switches { + display: flex; + flex-direction: column; + padding: 1rem; + } + + .device { + display: flex; + align-items: center; + border-radius: 2rem; + + .toggle { + text-align: right; + } + + &:nth-child(odd) { background: rgba(255, 255, 255, 0.0); } + &:nth-child(even) { background: $default-bg-3; } + &:hover { background: $hover-bg; } + } + } +} + diff --git a/platypush/backend/http/static/js/plugins/switches/index.js b/platypush/backend/http/static/js/plugins/switches/index.js new file mode 100644 index 000000000..f2a684298 --- /dev/null +++ b/platypush/backend/http/static/js/plugins/switches/index.js @@ -0,0 +1,99 @@ +const SwitchDevice = Vue.component('switch-device', { + template: '#tmpl-switch-device', + props: { + bus: { + type: Object, + }, + + device: { + type: Object, + default: () => {}, + }, + }, +}); + +const SwitchType = Vue.component('switch-type', { + template: '#tmpl-switch-type', + props: { + bus: { + type: Object, + }, + + name: { + type: String, + }, + + devices: { + type: Object, + default: () => {}, + }, + }, +}); + +Vue.component('switches', { + template: '#tmpl-switches', + props: ['config'], + + data: function() { + return { + bus: new Vue({}), + plugins: {}, + }; + }, + + methods: { + refresh: async function() { + if (!this.config.plugins) { + console.warn('Please specify a list of switch plugins in your switch section configuration'); + return; + } + + const promises = this.config.plugins.map(plugin => { + return new Promise((resolve, reject) => { + request(plugin + '.status').then(status => { + const ret = {}; + ret[plugin] = status; + resolve(ret); + }); + }); + }); + + const statuses = (await Promise.all(promises)).reduce((obj, status) => { + obj[Object.keys(status)[0]] = Object.values(status)[0].reduce((obj2, device) => { + device.type = Object.keys(status)[0]; + obj2[device.id] = device; + return obj2; + }, {}); + + return obj; + }, {}); + + for (const [name, status] of Object.entries(statuses)) { + this.plugins[name] = status; + + const switchType = new SwitchType(); + switchType.bus = this.bus; + switchType.name = name; + switchType.devices = this.plugins[name]; + + switchType.$mount(); + this.$refs.root.appendChild(switchType.$el); + } + }, + + toggle: async function(type, device) { + let status = await request(type + '.toggle', {device: device}); + this.plugins[type][status.id].on = status.on; + }, + }, + + mounted: function() { + const self = this; + this.refresh(); + + this.bus.$on('switch-toggled', (evt) => { + self.toggle(evt.type, evt.device); + }); + }, +}); + diff --git a/platypush/backend/http/templates/nav.html b/platypush/backend/http/templates/nav.html index b340f52e4..8cf60afd6 100644 --- a/platypush/backend/http/templates/nav.html +++ b/platypush/backend/http/templates/nav.html @@ -7,6 +7,7 @@ 'media.vlc': 'fa fa-film', 'music.mpd': 'fa fa-music', 'music.snapcast': 'fa fa-volume-up', + 'switches': 'fa fa-toggle-on', 'tts': 'fa fa-comment', 'tts.google': 'fa fa-comment', } @@ -20,7 +21,7 @@ {% if plugin in pluginIcons %} {% else %} - {% raw %}{{ plugin }}{{% endraw %} + {{ plugin }} {% endif %} diff --git a/platypush/backend/http/templates/plugins/switches/index.html b/platypush/backend/http/templates/plugins/switches/index.html new file mode 100644 index 000000000..f8241a4ee --- /dev/null +++ b/platypush/backend/http/templates/plugins/switches/index.html @@ -0,0 +1,27 @@ + + + + + + diff --git a/platypush/plugins/switch/__init__.py b/platypush/plugins/switch/__init__.py index b025807cc..9f4406109 100644 --- a/platypush/plugins/switch/__init__.py +++ b/platypush/plugins/switch/__init__.py @@ -1,33 +1,45 @@ from platypush.plugins import Plugin, action + class SwitchPlugin(Plugin): """ Abstract class for interacting with switch devices """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, **kwargs): + super().__init__(**kwargs) @action - def on(self, args): + def on(self, device, *args, **kwargs): """ Turn the device on """ raise NotImplementedError() @action - def off(self, args): + def off(self, device, *args, **kwargs): """ Turn the device off """ raise NotImplementedError() @action - def toggle(self, args): + def toggle(self, device, *args, **kwargs): """ Toggle the device status (on/off) """ raise NotImplementedError() @action - def status(self): - """ Get the device state """ + def status(self, device=None, *args, **kwargs): + """ Get the status of a specified device or of all the configured devices (default)""" + devices = self.devices + if device: + devices = [d for d in self.devices if d.get('id') == device or d.get('name') == device] + if devices: + return self.devices.pop(0) + else: + return None + + return devices + + @property + def devices(self): raise NotImplementedError() # vim:sw=4:ts=4:et: - diff --git a/platypush/plugins/switch/switchbot/__init__.py b/platypush/plugins/switch/switchbot/__init__.py index 13185dfd6..078dc7103 100644 --- a/platypush/plugins/switch/switchbot/__init__.py +++ b/platypush/plugins/switch/switchbot/__init__.py @@ -7,48 +7,52 @@ from bluetooth.ble import DiscoveryService, GATTRequester from platypush.plugins import action from platypush.plugins.switch import SwitchPlugin + class Scanner(object): + """ + XXX The Scanner object doesn't work. Add your devices by address statically to the plugin configuration for now + instead of relying on scanning capabilities + """ + service_uuid = '1bc5d5a5-0200b89f-e6114d22-000da2cb' def __init__(self, bt_interface=None, timeout_secs=None): self.bt_interface = bt_interface self.timeout_secs = timeout_secs if timeout_secs else 2 - @classmethod def _get_uuids(cls, device): uuids = set() - for id in device['uuids']: - if isinstance(id, tuple): + for uuid in device['uuids']: + if isinstance(uuid, tuple): uuid = '' - for i in range(0, len(id)): - token = struct.pack('= self.timeout_secs: raise RuntimeError('Connection to {} timed out after {} seconds' @@ -96,7 +100,7 @@ class SwitchSwitchbotPlugin(SwitchPlugin): """ def __init__(self, bt_interface=None, connect_timeout=None, - scan_timeout=None, devices={}, *args, **kwargs): + scan_timeout=None, devices=None, **kwargs): """ :param bt_interface: Bluetooth interface to use (e.g. hci0) default: first available one :type bt_interface: str @@ -111,20 +115,30 @@ class SwitchSwitchbotPlugin(SwitchPlugin): :type devices: dict """ - super().__init__(*args, **kwargs) + super().__init__(**kwargs) + + if devices is None: + devices = {} + self.bt_interface = bt_interface self.connect_timeout = connect_timeout if connect_timeout else 5 self.scan_timeout = scan_timeout if scan_timeout else 2 - self.devices = devices - + self.configured_devices = devices + self.configured_devices_by_name = { + name: addr + for addr, name in self.configured_devices.items() + } def _run(self, device, command=None): + if device in self.configured_devices_by_name: + device = self.configured_devices_by_name[device] + try: # XXX this requires sudo and it's executed in its own process # because the Switchbot plugin requires root privileges to send # raw bluetooth messages on the interface. Make sure that the user # that runs platypush has the right permissions to run this with sudo - return subprocess.check_output(( + output = subprocess.check_output(( 'sudo python3 -m platypush.plugins.switch.switchbot ' + '--device {} ' + ('--interface {} '.format(self.bt_interface) if self.bt_interface else '') + @@ -134,6 +148,8 @@ class SwitchSwitchbotPlugin(SwitchPlugin): except subprocess.CalledProcessError as e: raise RuntimeError(e.output.decode('utf-8')) + self.logger.info('Output of switchbot command: {}'.format(output)) + return self.status(device) @action def press(self, device): @@ -146,7 +162,11 @@ class SwitchSwitchbotPlugin(SwitchPlugin): return self._run(device) @action - def on(self, device): + def toggle(self, device, **kwargs): + return self.press(device) + + @action + def on(self, device, **kwargs): """ Send a press-on button command to a device @@ -156,7 +176,7 @@ class SwitchSwitchbotPlugin(SwitchPlugin): return self._run(device, 'on') @action - def off(self, device): + def off(self, device, **kwargs): """ Send a press-off button command to a device @@ -167,7 +187,11 @@ class SwitchSwitchbotPlugin(SwitchPlugin): @action def scan(self): - """ Scan for available Switchbot devices nearby """ + """ + Scan for available Switchbot devices nearby. + XXX This action doesn't work for now. Configure your devices statically for now instead of + relying on the scanner + """ try: return subprocess.check_output( 'sudo python3 -m platypush.plugins.switch.switchbot --scan ' + @@ -177,6 +201,17 @@ class SwitchSwitchbotPlugin(SwitchPlugin): except subprocess.CalledProcessError as e: raise RuntimeError(e.output.decode('utf-8')) + @property + def devices(self): + return [ + { + 'address': addr, + 'id': addr, + 'name': name, + 'on': False, + } + for addr, name in self.configured_devices.items() + ] + # vim:sw=4:ts=4:et: - diff --git a/platypush/plugins/switch/tplink.py b/platypush/plugins/switch/tplink.py index 6a8100dcf..818a48ea5 100644 --- a/platypush/plugins/switch/tplink.py +++ b/platypush/plugins/switch/tplink.py @@ -17,15 +17,14 @@ class SwitchTplinkPlugin(SwitchPlugin): _ip_to_dev = {} _alias_to_dev = {} - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, **kwargs): + super().__init__(**kwargs) def _scan(self): devices = Discover.discover() self._ip_to_dev = {} self._alias_to_dev = {} - for (ip, dev) in devices.items(): self._ip_to_dev[ip] = dev self._alias_to_dev[dev.alias] = dev @@ -47,27 +46,8 @@ class SwitchTplinkPlugin(SwitchPlugin): else: raise RuntimeError('Device {} not found'.format(device)) - @action - def status(self): - """ - :returns: The available device over the network as a - """ - - devices = { 'devices': { - ip: { - 'alias': dev.alias, - 'current_consumption': dev.current_consumption(), - 'host': dev.host, - 'hw_info': dev.hw_info, - 'on': dev.is_on, - } for (ip, dev) in self._scan().items() - } } - - return devices - - @action - def on(self, device): + def on(self, device, **kwargs): """ Turn on a device @@ -77,11 +57,10 @@ class SwitchTplinkPlugin(SwitchPlugin): device = self._get_device(device) device.turn_on() - return {'status':'on'} - + return self.status(device) @action - def off(self, device): + def off(self, device, **kwargs): """ Turn off a device @@ -91,11 +70,10 @@ class SwitchTplinkPlugin(SwitchPlugin): device = self._get_device(device) device.turn_off() - return {'status':'off'} - + return self.status(device) @action - def toggle(self, device): + def toggle(self, device, **kwargs): """ Toggle the state of a device (on/off) @@ -103,15 +81,36 @@ class SwitchTplinkPlugin(SwitchPlugin): :type device: str """ - device = self._get_device(device, use_cache=False) + device = self._get_device(device) if device.is_on: device.turn_off() else: device.turn_on() - return {'status': 'off' if device.is_off else 'on'} + 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, + } + + @property + def devices(self): + return [ + { + 'current_consumption': dev.current_consumption(), + 'id': ip, + 'ip': ip, + 'host': dev.host, + 'hw_info': dev.hw_info, + 'name': dev.alias, + 'on': dev.is_on, + } for (ip, dev) in self._scan().items() + ] # vim:sw=4:ts=4:et: - diff --git a/platypush/plugins/switch/wemo/__init__.py b/platypush/plugins/switch/wemo/__init__.py index c256cb4d4..636bd58b4 100644 --- a/platypush/plugins/switch/wemo/__init__.py +++ b/platypush/plugins/switch/wemo/__init__.py @@ -1,12 +1,10 @@ -import json - from platypush.plugins import action from platypush.plugins.switch import SwitchPlugin class SwitchWemoPlugin(SwitchPlugin): """ - Plugin to control a Belkin WeMo smart switch + Plugin to control a Belkin WeMo smart switches (https://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/) Requires: @@ -14,13 +12,13 @@ class SwitchWemoPlugin(SwitchPlugin): * **ouimeaux** (``pip install ouimeaux``) """ - def __init__(self, discovery_seconds=3, *args, **kwargs): + def __init__(self, discovery_seconds=3, **kwargs): """ :param discovery_seconds: Discovery time when scanning for devices (default: 3) :type discovery_seconds: int """ - super().__init__(*args, **kwargs) + super().__init__(**kwargs) self.discovery_seconds = discovery_seconds self.env = None @@ -29,7 +27,7 @@ class SwitchWemoPlugin(SwitchPlugin): self.logger.info('Starting WeMo discovery') self._get_environment() self.env.discover(seconds=self.discovery_seconds) - self.devices = self.env.devices + self._devices = self.env.devices def _get_environment(self): if not self.env: @@ -38,18 +36,17 @@ class SwitchWemoPlugin(SwitchPlugin): self.env.start() self._refresh_devices() - @action - def get_devices(self): + @property + def devices(self): """ Get the list of available devices :returns: The list of devices. Example output:: - output = { - "devices": [ + output = [ { - "host": "192.168.1.123", + "ip": "192.168.1.123", "name": "Switch 1", "state": 1, "model": "Belkin Plugin Socket 1.0", @@ -60,40 +57,39 @@ class SwitchWemoPlugin(SwitchPlugin): # ... } ] - } """ self._refresh_devices() - return { - 'devices': [ - { - 'host': dev.host, - 'name': dev.name, - 'state': dev.get_state(), - 'model': dev.model, - 'serialnumber': dev.serialnumber, - } - for (name, dev) in self.devices.items() - ] - } + + return [ + { + 'id': dev.name, + 'ip': dev.host, + 'name': dev.name, + 'model': dev.model, + 'on': True if dev.get_state() else False, + 'serialnumber': dev.serialnumber, + } + for (name, dev) in self._devices.items() + ] def _exec(self, method, device, *args, **kwargs): self._get_environment() - if device not in self.devices: + if device not in self._devices: self._refresh_devices() - if device not in self.devices: + if device not in self._devices: raise RuntimeError('Device {} not found'.format(device)) self.logger.info('Executing {} on WeMo device {}'. format(method, device)) - dev = self.devices[device] + dev = self._devices[device] getattr(dev, method)(*args, **kwargs) - return {'device': device, 'state': dev.get_state()} + return self.status(device) @action - def on(self, device): + def on(self, device, **kwargs): """ Turn a switch on @@ -103,7 +99,7 @@ class SwitchWemoPlugin(SwitchPlugin): return self._exec('on', device) @action - def off(self, device): + def off(self, device, **kwargs): """ Turn a switch off @@ -113,7 +109,7 @@ class SwitchWemoPlugin(SwitchPlugin): return self._exec('off', device) @action - def toggle(self, device): + def toggle(self, device, **kwargs): """ Toggle the state of a switch (on/off) @@ -124,4 +120,3 @@ class SwitchWemoPlugin(SwitchPlugin): # vim:sw=4:ts=4:et: -