Added switch tab to the new web panel

This commit is contained in:
Fabio Manganiello 2019-07-02 12:02:28 +02:00
parent 26ee3fc75c
commit b932df1c12
8 changed files with 299 additions and 95 deletions

View file

@ -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; }
}
}
}

View file

@ -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);
});
},
});

View file

@ -7,6 +7,7 @@
'media.vlc': 'fa fa-film', 'media.vlc': 'fa fa-film',
'music.mpd': 'fa fa-music', 'music.mpd': 'fa fa-music',
'music.snapcast': 'fa fa-volume-up', 'music.snapcast': 'fa fa-volume-up',
'switches': 'fa fa-toggle-on',
'tts': 'fa fa-comment', 'tts': 'fa fa-comment',
'tts.google': 'fa fa-comment', 'tts.google': 'fa fa-comment',
} }
@ -20,7 +21,7 @@
{% if plugin in pluginIcons %} {% if plugin in pluginIcons %}
<i class="{{ pluginIcons[plugin] }}"></i> <i class="{{ pluginIcons[plugin] }}"></i>
{% else %} {% else %}
{% raw %}{{ plugin }}{{% endraw %} {{ plugin }}
{% endif %} {% endif %}
</a> </a>
</li> </li>

View file

@ -0,0 +1,27 @@
<script type="text/x-template" id="tmpl-switches">
<div class="switches-root" ref="root"></div>
</script>
<script type="text/x-template" id="tmpl-switch-type">
<div class="switch-root" ref="root">
<div class="head" v-text="name.split('.').pop()"></div>
<div class="switches">
<switch-device :device="device"
v-for="device in devices"
:key="device.name"
:bus="bus">
</switch-device>
</div>
</div>
</script>
<script type="text/x-template" id="tmpl-switch-device">
<div class="device">
<div class="col-10 name" v-text="device.name"></div>
<div class="col-2 toggle">
<toggle-switch :value="device.on" @toggled="bus.$emit('switch-toggled', {type: device.type, device: device.id})"></toggle-switch>
</div>
</div>
</script>

View file

@ -1,33 +1,45 @@
from platypush.plugins import Plugin, action from platypush.plugins import Plugin, action
class SwitchPlugin(Plugin): class SwitchPlugin(Plugin):
""" """
Abstract class for interacting with switch devices Abstract class for interacting with switch devices
""" """
def __init__(self, *args, **kwargs): def __init__(self, **kwargs):
super().__init__(*args, **kwargs) super().__init__(**kwargs)
@action @action
def on(self, args): def on(self, device, *args, **kwargs):
""" Turn the device on """ """ Turn the device on """
raise NotImplementedError() raise NotImplementedError()
@action @action
def off(self, args): def off(self, device, *args, **kwargs):
""" Turn the device off """ """ Turn the device off """
raise NotImplementedError() raise NotImplementedError()
@action @action
def toggle(self, args): def toggle(self, device, *args, **kwargs):
""" Toggle the device status (on/off) """ """ Toggle the device status (on/off) """
raise NotImplementedError() raise NotImplementedError()
@action @action
def status(self): def status(self, device=None, *args, **kwargs):
""" Get the device state """ """ 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() raise NotImplementedError()
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -7,33 +7,37 @@ from bluetooth.ble import DiscoveryService, GATTRequester
from platypush.plugins import action from platypush.plugins import action
from platypush.plugins.switch import SwitchPlugin from platypush.plugins.switch import SwitchPlugin
class Scanner(object): 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' service_uuid = '1bc5d5a5-0200b89f-e6114d22-000da2cb'
def __init__(self, bt_interface=None, timeout_secs=None): def __init__(self, bt_interface=None, timeout_secs=None):
self.bt_interface = bt_interface self.bt_interface = bt_interface
self.timeout_secs = timeout_secs if timeout_secs else 2 self.timeout_secs = timeout_secs if timeout_secs else 2
@classmethod @classmethod
def _get_uuids(cls, device): def _get_uuids(cls, device):
uuids = set() uuids = set()
for id in device['uuids']: for uuid in device['uuids']:
if isinstance(id, tuple): if isinstance(uuid, tuple):
uuid = '' uuid = ''
for i in range(0, len(id)): for i in range(0, len(uuid)):
token = struct.pack('<I', id[i]) token = struct.pack('<I', uuid[i])
for byte in token: for byte in token:
uuid += hex(byte)[2:].zfill(2) uuid += hex(byte)[2:].zfill(2)
uuid += ('-' if i < len(id)-1 else '') uuid += ('-' if i < len(uuid)-1 else '')
uuids.add(uuid) uuids.add(uuid)
else: else:
uuids.add(hex(id)[2:]) uuids.add(hex(uuid)[2:])
return uuids return uuids
def scan(self): def scan(self):
service = DiscoveryService(self.bt_interface) \ service = DiscoveryService(self.bt_interface) \
if self.bt_interface else DiscoveryService() if self.bt_interface else DiscoveryService()
@ -46,9 +50,9 @@ class Scanner(object):
class Driver(object): class Driver(object):
handle = 0x16 handle = 0x16
commands = { commands = {
'press' : '\x57\x01\x00', 'press': '\x57\x01\x00',
'on' : '\x57\x01\x01', 'on': '\x57\x01\x01',
'off' : '\x57\x01\x02', 'off': '\x57\x01\x02',
} }
def __init__(self, device, bt_interface=None, timeout_secs=None): def __init__(self, device, bt_interface=None, timeout_secs=None):
@ -57,7 +61,6 @@ class Driver(object):
self.timeout_secs = timeout_secs if timeout_secs else 5 self.timeout_secs = timeout_secs if timeout_secs else 5
self.req = None self.req = None
def connect(self): def connect(self):
if self.bt_interface: if self.bt_interface:
self.req = GATTRequester(self.device, False, self.bt_interface) self.req = GATTRequester(self.device, False, self.bt_interface)
@ -66,6 +69,7 @@ class Driver(object):
self.req.connect(True, 'random') self.req.connect(True, 'random')
connect_start_time = time.time() connect_start_time = time.time()
while not self.req.is_connected(): while not self.req.is_connected():
if time.time() - connect_start_time >= self.timeout_secs: if time.time() - connect_start_time >= self.timeout_secs:
raise RuntimeError('Connection to {} timed out after {} seconds' raise RuntimeError('Connection to {} timed out after {} seconds'
@ -96,7 +100,7 @@ class SwitchSwitchbotPlugin(SwitchPlugin):
""" """
def __init__(self, bt_interface=None, connect_timeout=None, 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 :param bt_interface: Bluetooth interface to use (e.g. hci0) default: first available one
:type bt_interface: str :type bt_interface: str
@ -111,20 +115,30 @@ class SwitchSwitchbotPlugin(SwitchPlugin):
:type devices: dict :type devices: dict
""" """
super().__init__(*args, **kwargs) super().__init__(**kwargs)
if devices is None:
devices = {}
self.bt_interface = bt_interface self.bt_interface = bt_interface
self.connect_timeout = connect_timeout if connect_timeout else 5 self.connect_timeout = connect_timeout if connect_timeout else 5
self.scan_timeout = scan_timeout if scan_timeout else 2 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): def _run(self, device, command=None):
if device in self.configured_devices_by_name:
device = self.configured_devices_by_name[device]
try: try:
# XXX this requires sudo and it's executed in its own process # XXX this requires sudo and it's executed in its own process
# because the Switchbot plugin requires root privileges to send # because the Switchbot plugin requires root privileges to send
# raw bluetooth messages on the interface. Make sure that the user # raw bluetooth messages on the interface. Make sure that the user
# that runs platypush has the right permissions to run this with sudo # 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 ' + 'sudo python3 -m platypush.plugins.switch.switchbot ' +
'--device {} ' + '--device {} ' +
('--interface {} '.format(self.bt_interface) if self.bt_interface else '') + ('--interface {} '.format(self.bt_interface) if self.bt_interface else '') +
@ -134,6 +148,8 @@ class SwitchSwitchbotPlugin(SwitchPlugin):
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
raise RuntimeError(e.output.decode('utf-8')) raise RuntimeError(e.output.decode('utf-8'))
self.logger.info('Output of switchbot command: {}'.format(output))
return self.status(device)
@action @action
def press(self, device): def press(self, device):
@ -146,7 +162,11 @@ class SwitchSwitchbotPlugin(SwitchPlugin):
return self._run(device) return self._run(device)
@action @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 Send a press-on button command to a device
@ -156,7 +176,7 @@ class SwitchSwitchbotPlugin(SwitchPlugin):
return self._run(device, 'on') return self._run(device, 'on')
@action @action
def off(self, device): def off(self, device, **kwargs):
""" """
Send a press-off button command to a device Send a press-off button command to a device
@ -167,7 +187,11 @@ class SwitchSwitchbotPlugin(SwitchPlugin):
@action @action
def scan(self): 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: try:
return subprocess.check_output( return subprocess.check_output(
'sudo python3 -m platypush.plugins.switch.switchbot --scan ' + 'sudo python3 -m platypush.plugins.switch.switchbot --scan ' +
@ -177,6 +201,17 @@ class SwitchSwitchbotPlugin(SwitchPlugin):
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
raise RuntimeError(e.output.decode('utf-8')) 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: # vim:sw=4:ts=4:et:

View file

@ -17,15 +17,14 @@ class SwitchTplinkPlugin(SwitchPlugin):
_ip_to_dev = {} _ip_to_dev = {}
_alias_to_dev = {} _alias_to_dev = {}
def __init__(self, *args, **kwargs): def __init__(self, **kwargs):
super().__init__(*args, **kwargs) super().__init__(**kwargs)
def _scan(self): def _scan(self):
devices = Discover.discover() devices = Discover.discover()
self._ip_to_dev = {} self._ip_to_dev = {}
self._alias_to_dev = {} self._alias_to_dev = {}
for (ip, dev) in devices.items(): for (ip, dev) in devices.items():
self._ip_to_dev[ip] = dev self._ip_to_dev[ip] = dev
self._alias_to_dev[dev.alias] = dev self._alias_to_dev[dev.alias] = dev
@ -47,27 +46,8 @@ class SwitchTplinkPlugin(SwitchPlugin):
else: else:
raise RuntimeError('Device {} not found'.format(device)) raise RuntimeError('Device {} not found'.format(device))
@action @action
def status(self): def on(self, device, **kwargs):
"""
: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):
""" """
Turn on a device Turn on a device
@ -77,11 +57,10 @@ class SwitchTplinkPlugin(SwitchPlugin):
device = self._get_device(device) device = self._get_device(device)
device.turn_on() device.turn_on()
return {'status':'on'} return self.status(device)
@action @action
def off(self, device): def off(self, device, **kwargs):
""" """
Turn off a device Turn off a device
@ -91,11 +70,10 @@ class SwitchTplinkPlugin(SwitchPlugin):
device = self._get_device(device) device = self._get_device(device)
device.turn_off() device.turn_off()
return {'status':'off'} return self.status(device)
@action @action
def toggle(self, device): def toggle(self, device, **kwargs):
""" """
Toggle the state of a device (on/off) Toggle the state of a device (on/off)
@ -103,15 +81,36 @@ class SwitchTplinkPlugin(SwitchPlugin):
:type device: str :type device: str
""" """
device = self._get_device(device, use_cache=False) device = self._get_device(device)
if device.is_on: if device.is_on:
device.turn_off() device.turn_off()
else: else:
device.turn_on() 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: # vim:sw=4:ts=4:et:

View file

@ -1,12 +1,10 @@
import json
from platypush.plugins import action from platypush.plugins import action
from platypush.plugins.switch import SwitchPlugin from platypush.plugins.switch import SwitchPlugin
class SwitchWemoPlugin(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/) (https://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/)
Requires: Requires:
@ -14,13 +12,13 @@ class SwitchWemoPlugin(SwitchPlugin):
* **ouimeaux** (``pip install ouimeaux``) * **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) :param discovery_seconds: Discovery time when scanning for devices (default: 3)
:type discovery_seconds: int :type discovery_seconds: int
""" """
super().__init__(*args, **kwargs) super().__init__(**kwargs)
self.discovery_seconds = discovery_seconds self.discovery_seconds = discovery_seconds
self.env = None self.env = None
@ -29,7 +27,7 @@ class SwitchWemoPlugin(SwitchPlugin):
self.logger.info('Starting WeMo discovery') self.logger.info('Starting WeMo discovery')
self._get_environment() self._get_environment()
self.env.discover(seconds=self.discovery_seconds) self.env.discover(seconds=self.discovery_seconds)
self.devices = self.env.devices self._devices = self.env.devices
def _get_environment(self): def _get_environment(self):
if not self.env: if not self.env:
@ -38,18 +36,17 @@ class SwitchWemoPlugin(SwitchPlugin):
self.env.start() self.env.start()
self._refresh_devices() self._refresh_devices()
@action @property
def get_devices(self): def devices(self):
""" """
Get the list of available devices Get the list of available devices
:returns: The list of devices. :returns: The list of devices.
Example output:: Example output::
output = { output = [
"devices": [
{ {
"host": "192.168.1.123", "ip": "192.168.1.123",
"name": "Switch 1", "name": "Switch 1",
"state": 1, "state": 1,
"model": "Belkin Plugin Socket 1.0", "model": "Belkin Plugin Socket 1.0",
@ -60,40 +57,39 @@ class SwitchWemoPlugin(SwitchPlugin):
# ... # ...
} }
] ]
}
""" """
self._refresh_devices() self._refresh_devices()
return {
'devices': [ return [
{ {
'host': dev.host, 'id': dev.name,
'ip': dev.host,
'name': dev.name, 'name': dev.name,
'state': dev.get_state(),
'model': dev.model, 'model': dev.model,
'on': True if dev.get_state() else False,
'serialnumber': dev.serialnumber, 'serialnumber': dev.serialnumber,
} }
for (name, dev) in self.devices.items() for (name, dev) in self._devices.items()
] ]
}
def _exec(self, method, device, *args, **kwargs): def _exec(self, method, device, *args, **kwargs):
self._get_environment() self._get_environment()
if device not in self.devices: if device not in self._devices:
self._refresh_devices() self._refresh_devices()
if device not in self.devices: if device not in self._devices:
raise RuntimeError('Device {} not found'.format(device)) raise RuntimeError('Device {} not found'.format(device))
self.logger.info('Executing {} on WeMo device {}'. self.logger.info('Executing {} on WeMo device {}'.
format(method, device)) format(method, device))
dev = self.devices[device] dev = self._devices[device]
getattr(dev, method)(*args, **kwargs) getattr(dev, method)(*args, **kwargs)
return {'device': device, 'state': dev.get_state()} return self.status(device)
@action @action
def on(self, device): def on(self, device, **kwargs):
""" """
Turn a switch on Turn a switch on
@ -103,7 +99,7 @@ class SwitchWemoPlugin(SwitchPlugin):
return self._exec('on', device) return self._exec('on', device)
@action @action
def off(self, device): def off(self, device, **kwargs):
""" """
Turn a switch off Turn a switch off
@ -113,7 +109,7 @@ class SwitchWemoPlugin(SwitchPlugin):
return self._exec('off', device) return self._exec('off', device)
@action @action
def toggle(self, device): def toggle(self, device, **kwargs):
""" """
Toggle the state of a switch (on/off) Toggle the state of a switch (on/off)
@ -124,4 +120,3 @@ class SwitchWemoPlugin(SwitchPlugin):
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et: