platypush/platypush/backend/zwave/__init__.py

263 lines
14 KiB
Python
Raw Normal View History

2020-02-05 22:26:52 +01:00
import inspect
import logging
import queue
import os
import threading
from typing import Optional
from platypush.backend import Backend
from platypush.config import Config
from platypush.message.event.zwave import ZwaveNetworkReadyEvent, ZwaveNetworkStoppedEvent, ZwaveEvent, \
ZwaveNodeAddedEvent, ZwaveValueAddedEvent, ZwaveNodeQueryCompletedEvent, ZwaveValueChangedEvent, \
ZwaveValueRefreshedEvent, ZwaveValueRemovedEvent, ZwaveNetworkResetEvent, ZwaveCommandEvent, \
ZwaveCommandWaitingEvent, ZwaveNodeRemovedEvent, ZwaveNodeRenamedEvent, ZwaveNodeReadyEvent, \
ZwaveButtonRemovedEvent, ZwaveButtonCreatedEvent, ZwaveButtonOnEvent, ZwaveButtonOffEvent, ZwaveNetworkErrorEvent, \
ZwaveNodeGroupEvent, ZwaveNodePollingEnabledEvent, ZwaveNodePollingDisabledEvent, ZwaveNodeSceneEvent, \
ZwaveNodeEvent
event_queue = queue.Queue()
network_ready = threading.Event()
class _ZWEvent:
def __init__(self, signal: str, sender: str, network=None, **kwargs):
self.signal = signal
self.sender = sender
self.network = network
self.args = kwargs
def _zwcallback(signal, sender, network, **kwargs):
if signal == network.SIGNAL_NETWORK_AWAKED:
network_ready.set()
event_queue.put(_ZWEvent(signal=signal, sender=sender, network=network, **kwargs))
class ZwaveBackend(Backend):
"""
Start and manage a Z-Wave network.
If you are using a USB adapter and want a consistent naming for the device paths, you can use udev.
.. code-block:: shell
# Get the vendorID and productID of your device through lsusb.
# Then add a udev rule for it to link it e.g. to /dev/zwave.
cat <<EOF > /etc/udev/rules.d/92-zwave.rules
SUBSYSTEM=="tty", ATTRS{idVendor}=="0658", ATTRS{idProduct}=="0200", SYMLINK+="zwave"
EOF
# Restart the udev service
systemctl restart systemd-udevd.service
Triggers:
* :class:`platypush.message.event.zwave.ZwaveNetworkReadyEvent` when the network is up and running.
* :class:`platypush.message.event.zwave.ZwaveNetworkStoppedEvent` when the network goes down.
* :class:`platypush.message.event.zwave.ZwaveNetworkResetEvent` when the network is reset.
* :class:`platypush.message.event.zwave.ZwaveNetworkErrorEvent` when an error occurs on the network.
* :class:`platypush.message.event.zwave.ZwaveNodeQueryCompletedEvent` when all the nodes on the network
have been queried.
* :class:`platypush.message.event.zwave.ZwaveNodeEvent` when a node attribute changes.
* :class:`platypush.message.event.zwave.ZwaveNodeAddedEvent` when a node is added to the network.
* :class:`platypush.message.event.zwave.ZwaveNodeRemovedEvent` when a node is removed from the network.
* :class:`platypush.message.event.zwave.ZwaveNodeRenamedEvent` when a node is renamed.
* :class:`platypush.message.event.zwave.ZwaveNodeReadyEvent` when a node is ready.
* :class:`platypush.message.event.zwave.ZwaveNodeGroupEvent` when a node is associated/de-associated to a
group.
* :class:`platypush.message.event.zwave.ZwaveNodeSceneEvent` when a scene is set on a node.
* :class:`platypush.message.event.zwave.ZwaveNodePollingEnabledEvent` when the polling is successfully turned
on a node.
* :class:`platypush.message.event.zwave.ZwaveNodePollingDisabledEvent` when the polling is successfully turned
off a node.
* :class:`platypush.message.event.zwave.ZwaveButtonCreatedEvent` when a button is added to the network.
* :class:`platypush.message.event.zwave.ZwaveButtonRemovedEvent` when a button is removed from the network.
* :class:`platypush.message.event.zwave.ZwaveButtonOnEvent` when a button is pressed.
* :class:`platypush.message.event.zwave.ZwaveButtonOffEvent` when a button is released.
* :class:`platypush.message.event.zwave.ZwaveValueAddedEvent` when a value is added to a node on the network.
* :class:`platypush.message.event.zwave.ZwaveValueChangedEvent` when the value of a node on the network
changes.
* :class:`platypush.message.event.zwave.ZwaveValueRefreshedEvent` when the value of a node on the network
is refreshed.
* :class:`platypush.message.event.zwave.ZwaveValueRemovedEvent` when the value of a node on the network
is removed.
* :class:`platypush.message.event.zwave.ZwaveCommandEvent` when a command is received on the network.
* :class:`platypush.message.event.zwave.ZwaveCommandWaitingEvent` when a command is waiting for a message
to complete.
Requires:
* **python-openzwave** (``pip install python-openzwave``)
"""
def __init__(self, device: str, config_path: Optional[str] = None, user_path: Optional[str] = None,
ready_timeout: float = 10.0, *args, **kwargs):
"""
:param device: Path to the Z-Wave adapter (e.g. /dev/ttyUSB0 or /dev/ttyACM0).
:param config_path: Z-Wave configuration path (default: ``<OPENZWAVE_PATH>/ozw_config``).
:param user_path: Z-Wave user path where runtime and configuration files will be stored
(default: ``<PLATYPUSH_WORKDIR>/zwave``).
:param ready_timeout: Network ready timeout in seconds (default: 60).
"""
import python_openzwave
from openzwave.network import ZWaveNetwork
super().__init__(*args, **kwargs)
self.device = device
if not config_path:
config_path = os.path.join(os.path.dirname(inspect.getfile(python_openzwave)), 'ozw_config')
if not user_path:
user_path = os.path.join(Config.get('workdir'), 'zwave')
os.makedirs(user_path, mode=0o770, exist_ok=True)
self.config_path = config_path
self.user_path = user_path
self.ready_timeout = ready_timeout
self.network: Optional[ZWaveNetwork] = None
def start_network(self):
if self.network and self.network.state >= self.network.STATE_AWAKED:
self.logger.info('Z-Wave network already started')
return
from openzwave.network import ZWaveNetwork, dispatcher
from openzwave.option import ZWaveOption
network_ready.clear()
logging.getLogger('openzwave').addHandler(self.logger)
opts = ZWaveOption(self.device, config_path=self.config_path, user_path=self.user_path)
opts.set_console_output(False)
opts.lock()
self.network = ZWaveNetwork(opts, log=None)
dispatcher.connect(_zwcallback)
ready = network_ready.wait(self.ready_timeout)
if not ready:
self.logger.warning('Driver not ready after {} seconds: continuing anyway'.format(self.ready_timeout))
def stop_network(self):
if self.network:
self.network.stop()
network_ready.clear()
self.network = None
def _process_event(self, event: _ZWEvent):
from platypush.plugins.zwave import ZwavePlugin
network = event.network or self.network
if event.signal == network.SIGNAL_NETWORK_STOPPED or \
event.signal == network.SIGNAL_DRIVER_REMOVED:
event_queue.put(ZwaveNetworkStoppedEvent(device=self.device))
elif event.signal == network.SIGNAL_ALL_NODES_QUERIED or \
event.signal == network.SIGNAL_ALL_NODES_QUERIED_SOME_DEAD:
event = ZwaveNodeQueryCompletedEvent(device=self.device)
elif event.signal == network.SIGNAL_NETWORK_FAILED:
event = ZwaveNetworkErrorEvent(device=self.device)
self.logger.warning('Z-Wave network error')
elif event.signal == network.SIGNAL_NETWORK_RESETTED or \
event.signal == network.SIGNAL_DRIVER_RESET:
event = ZwaveNetworkResetEvent(device=self.device)
elif event.signal == network.SIGNAL_BUTTON_ON:
event = ZwaveButtonOnEvent(device=self.device,
node=ZwavePlugin.node_to_dict(event.args['node']))
elif event.signal == network.SIGNAL_BUTTON_OFF:
event = ZwaveButtonOffEvent(device=self.device,
node=ZwavePlugin.node_to_dict(event.args['node']))
elif event.signal == network.SIGNAL_CONTROLLER_COMMAND:
event = ZwaveCommandEvent(device=self.device,
state=event.args['state'],
state_description=event.args['state_full'],
error=event.args['error'] if event.args['error_int'] else None,
error_description=event.args['error_full'] if event.args['error_int'] else None,
node=ZwavePlugin.node_to_dict(event.args['node']) if event.args['node'] else None)
elif event.signal == network.SIGNAL_CONTROLLER_WAITING:
event = ZwaveCommandWaitingEvent(device=self.device,
state=event.args['state'],
state_description=event.args['state_full'])
elif event.signal == network.SIGNAL_CREATE_BUTTON:
event = ZwaveButtonCreatedEvent(device=self.device,
node=ZwavePlugin.node_to_dict(event.args['node']))
elif event.signal == network.SIGNAL_DELETE_BUTTON:
event = ZwaveButtonRemovedEvent(device=self.device,
node=ZwavePlugin.node_to_dict(event.args['node']))
elif event.signal == network.SIGNAL_GROUP:
event = ZwaveNodeGroupEvent(device=self.device,
node=ZwavePlugin.node_to_dict(event.args['node']),
group_index=event.args['groupidx'])
elif event.signal == network.SIGNAL_NETWORK_AWAKED:
event = ZwaveNetworkReadyEvent(
device=self.device,
ozw_library_version=self.network.controller.ozw_library_version,
python_library_version=self.network.controller.python_library_version,
zwave_library=self.network.controller.library_description,
home_id=self.network.controller.home_id,
node_id=self.network.controller.node_id,
node_version=self.network.controller.node.version,
nodes_count=self.network.nodes_count,
)
elif event.signal == network.SIGNAL_NODE_EVENT:
event = ZwaveNodeEvent(device=self.device,
node=ZwavePlugin.node_to_dict(event.args['node']))
elif event.signal == network.SIGNAL_NODE_ADDED:
event = ZwaveNodeAddedEvent(device=self.device,
node=ZwavePlugin.node_to_dict(event.args['node']))
elif event.signal == network.SIGNAL_NODE_NAMING:
event = ZwaveNodeRenamedEvent(device=self.device,
node=ZwavePlugin.node_to_dict(event.args['node']))
elif event.signal == network.SIGNAL_NODE_READY:
event = ZwaveNodeReadyEvent(device=self.device,
node=ZwavePlugin.node_to_dict(event.args['node']))
elif event.signal == network.SIGNAL_NODE_REMOVED:
event = ZwaveNodeRemovedEvent(device=self.device,
node=ZwavePlugin.node_to_dict(event.args['node']))
elif event.signal == network.SIGNAL_POLLING_DISABLED:
event = ZwaveNodePollingEnabledEvent(device=self.device,
node=ZwavePlugin.node_to_dict(event.args['node']))
elif event.signal == network.SIGNAL_POLLING_ENABLED:
event = ZwaveNodePollingDisabledEvent(device=self.device,
node=ZwavePlugin.node_to_dict(event.args['node']))
elif event.signal == network.SIGNAL_SCENE_EVENT:
event = ZwaveNodeSceneEvent(device=self.device,
scene_id=event.args['scene_id'],
node=ZwavePlugin.node_to_dict(event.args['node']))
elif event.signal == network.SIGNAL_VALUE_ADDED:
event = ZwaveValueAddedEvent(device=self.device,
node=ZwavePlugin.node_to_dict(event.args['node']),
value=ZwavePlugin.value_to_dict(event.args['value']))
elif event.signal == network.SIGNAL_VALUE_CHANGED:
event = ZwaveValueChangedEvent(device=self.device,
node=ZwavePlugin.node_to_dict(event.args['node']),
value=ZwavePlugin.value_to_dict(event.args['value']))
elif event.signal == network.SIGNAL_VALUE_REFRESHED:
event = ZwaveValueRefreshedEvent(device=self.device,
node=ZwavePlugin.node_to_dict(event.args['node']),
value=ZwavePlugin.value_to_dict(event.args['value']))
elif event.signal == network.SIGNAL_VALUE_REMOVED:
event = ZwaveValueRemovedEvent(device=self.device,
node=ZwavePlugin.node_to_dict(event.args['node']),
value=ZwavePlugin.value_to_dict(event.args['value']))
if isinstance(event, ZwaveEvent):
self.bus.post(event)
def __enter__(self):
self.start_network()
def __exit__(self, exc_type, exc_val, exc_tb):
self.stop_network()
def loop(self):
try:
event = event_queue.get(block=True, timeout=1.0)
self._process_event(event)
except queue.Empty:
pass
# vim:sw=4:ts=4:et: