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 00000000..55a87a2f
--- /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 00000000..f2a68429
--- /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 b340f52e..8cf60afd 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 00000000..f8241a4e
--- /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 b025807c..9f440610 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 13185dfd..078dc710 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 6a8100dc..818a48ea 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 c256cb4d..636bd58b 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:
-