platypush/platypush/backend/zwave/__init__.py

263 lines
14 KiB
Python

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: