Support for commands handling

This commit is contained in:
zewelor
2017-11-26 21:57:12 +01:00
parent 09d1332b92
commit 6a1c7b0181
8 changed files with 160 additions and 70 deletions
+2 -1
View File
@@ -7,5 +7,6 @@ workers:
args: args:
devices: devices:
bedroom: 00:11:22:33:44:55 bedroom: 00:11:22:33:44:55
topic_prefix: thermostats topic_prefix: thermostat
topic_subscription: thermostat
update_interval: 60 update_interval: 60
+6 -17
View File
@@ -6,11 +6,10 @@ if sys.version_info < (3,4):
from logger import _LOGGER from logger import _LOGGER
import argparse import argparse
from queue import Queue from workers_queue import _WORKERS_QUEUE
from config import settings from config import settings
from mqtt import MqttClient from mqtt import MqttClient
from workers_manager import WorkersManager from workers_manager import WorkersManager
from apscheduler.schedulers.background import BackgroundScheduler
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('-d', '--debug', action='store_true', default=False) parser.add_argument('-d', '--debug', action='store_true', default=False)
@@ -21,31 +20,21 @@ if parsed.debug:
else: else:
_LOGGER.setLevel(logging.INFO) _LOGGER.setLevel(logging.INFO)
queue = Queue()
scheduler = BackgroundScheduler()
mqtt = MqttClient(settings['mqtt'])
manager = WorkersManager(settings['workers'])
_LOGGER.info('Starting') _LOGGER.info('Starting')
for worker_name, interval in manager.interval_enabled_workers.items(): mqtt = MqttClient(settings['mqtt'])
_LOGGER.debug("Added: %s with %d seconds interval" % (worker_name, interval)) manager = WorkersManager(settings['workers'], mqtt)
queue.put([worker_name])
scheduler.add_job(lambda x: queue.put([x]), 'interval', seconds=interval, args=[worker_name])
scheduler.start()
running = True running = True
try: try:
while running: while running:
for worker_name in queue.get(block=True):
try: try:
mqtt.publish(manager.update(worker_name)) mqtt.publish(_WORKERS_QUEUE.get(block=True).execute())
except (KeyboardInterrupt, SystemExit): except (KeyboardInterrupt, SystemExit):
raise raise
except: except Exception as e:
_LOGGER.info('Some error') _LOGGER.exception(e)
except KeyboardInterrupt: except KeyboardInterrupt:
running = False running = False
_LOGGER.info('Exiting allowing jobs to finish. If you need force exit use kill') _LOGGER.info('Exiting allowing jobs to finish. If you need force exit use kill')
+12 -2
View File
@@ -1,12 +1,11 @@
from logger import _LOGGER
import paho.mqtt.publish as publish import paho.mqtt.publish as publish
import paho.mqtt.client as mqtt
class MqttClient: class MqttClient:
def __init__(self, config): def __init__(self, config):
self._config = config self._config = config
def publish(self, messages): def publish(self, messages):
_LOGGER.debug(messages)
publish.multiple(list(map(lambda m: m.as_dict, messages)), publish.multiple(list(map(lambda m: m.as_dict, messages)),
hostname=self.hostname, hostname=self.hostname,
auth={'username': self.username, 'password': self.password}) auth={'username': self.username, 'password': self.password})
@@ -23,6 +22,17 @@ class MqttClient:
def password(self): def password(self):
return self._config['password'] return self._config['password']
def callbacks_subscription(self, callbacks):
mqttc = mqtt.Client()
mqttc.username_pw_set(self.username, self.password)
mqttc.connect(self.hostname)
for topic, callback in callbacks:
mqttc.message_callback_add(topic, callback)
mqttc.subscribe(topic)
mqttc.loop_start()
class MqttMessage: class MqttMessage:
def __init__(self, topic=None, payload=None): def __init__(self, topic=None, payload=None):
+17
View File
@@ -0,0 +1,17 @@
from abc import abstractmethod
class BaseWorker:
def __init__(self, **args):
for arg, value in args.items():
setattr(self, arg, value)
self._setup()
def _setup(self):
return
@abstractmethod
def status_update(self, **args):
...
def format_topic(self, *args):
return '/'.join([self.topic_prefix, *args])
+5 -5
View File
@@ -2,23 +2,22 @@ import time
from interruptingcow import timeout from interruptingcow import timeout
from mqtt import MqttMessage from mqtt import MqttMessage
from workers.base import BaseWorker
REQUIREMENTS = ['bluepy'] REQUIREMENTS = ['bluepy']
# Bluepy might need special settings # Bluepy might need special settings
# sudo setcap 'cap_net_raw,cap_net_admin+eip' /usr/local/lib/python3.6/dist-packages/bluepy/bluepy-helper # sudo setcap 'cap_net_raw,cap_net_admin+eip' /usr/local/lib/python3.6/dist-packages/bluepy/bluepy-helper
class MiscaleWorker(): class MiscaleWorker(BaseWorker):
def __init__(self, mac):
self._mac = mac
def status_update(self): def status_update(self):
return [MqttMessage(topic='weight/kg', payload=self._get_weight())] return [MqttMessage(topic='weight/kg', payload=self._get_weight())]
def _get_weight(self): def _get_weight(self):
from bluepy import btle from bluepy import btle
scan_processor = ScanProcessor(self._mac) scan_processor = ScanProcessor(self.mac)
scanner = btle.Scanner().withDelegate(scan_processor) scanner = btle.Scanner().withDelegate(scan_processor)
scanner.scan(5, passive=True) scanner.scan(5, passive=True)
@@ -29,6 +28,7 @@ class MiscaleWorker():
return -1 return -1
class ScanProcessor(): class ScanProcessor():
def __init__(self, mac): def __init__(self, mac):
self._mac = mac self._mac = mac
+76 -19
View File
@@ -1,41 +1,98 @@
from builtins import staticmethod
from mqtt import MqttMessage from mqtt import MqttMessage
from workers.base import BaseWorker
REQUIREMENTS = ['python-eq3bt'] REQUIREMENTS = ['python-eq3bt']
monitoredAttrs = ["low_battery", "valve_state", "target_temperature"] monitoredAttrs = ["low_battery", "valve_state", "target_temperature"]
class ThermostatWorker: STATE_BOOST = 'boost'
def __init__(self, devices): STATE_AWAY = 'away'
self._devices = devices STATE_MANUAL = 'manual'
def status_update(self): class ThermostatWorker(BaseWorker):
from eq3bt import Thermostat, Mode class ModesMapper():
from eq3bt import Mode
haModesMapping = { MAPPED_MODES = {
Mode.Closed: 'off', Mode.Closed: 'off',
Mode.Open: 'on', Mode.Open: 'on',
Mode.Auto: 'auto', Mode.Auto: 'auto',
Mode.Manual: 'manual', Mode.Manual: STATE_MANUAL,
Mode.Away: 'away', Mode.Away: STATE_AWAY,
Mode.Boost: 'boost', Mode.Boost: STATE_BOOST,
} }
ret = [] REVERSED_MODES = {v: k for k, v in MAPPED_MODES.items()}
@classmethod
def get_mapping(cls, mode):
return cls.MAPPED_MODES[mode]
@classmethod
def get_reverse_mapping(cls, mode):
return cls.REVERSED_MODES[mode]
@staticmethod
def away_mode_on_off(mode):
if mode == STATE_AWAY:
return 'ON'
else:
return 'OFF'
@staticmethod
def on_off_to_mode(on_off):
if on_off == 'ON':
return STATE_AWAY
else:
return 'auto'
def _setup(self):
from eq3bt import Thermostat
for name, mac in self.devices.items(): for name, mac in self.devices.items():
thermostat = Thermostat(mac) self.devices[name] = Thermostat(mac)
thermostat.update()
for attr in monitoredAttrs:
ret.append(MqttMessage(topic=self.format_topic(name, attr), payload=getattr(thermostat, attr)))
ret.append(MqttMessage(topic=self.format_topic(name, 'mode'), payload=haModesMapping[thermostat.mode])) def status_update(self):
ret.append(MqttMessage(topic=self.format_topic(name, 'away'), payload='ON' if thermostat.mode == Mode.Away else 'OFF')) ret = []
for name, thermostat in self.devices.items():
ret += self.update_device_state(name, thermostat)
return ret return ret
@property def on_command(self, topic, value):
def devices(self): _, device_name, method = topic.split('/')
return self._devices thermostat = self.devices[device_name]
if method == STATE_AWAY:
method = "mode"
value = ModesMapper.on_off_to_mode(value)
# It needs to be on separate if because first if can change method
if method == "mode":
value = ModesMapper.get_reverse_mapping(value)
elif method == "target_temperature":
value = float(value)
setattr(thermostat, method, value)
return self.update_device_state(device_name, thermostat)
def update_device_state(self, name, thermostat):
thermostat.update()
ret = []
for attr in monitoredAttrs:
ret.append(MqttMessage(topic=self.format_topic(name, attr), payload=getattr(thermostat, attr)))
ret.append(MqttMessage(topic=self.format_topic(name, 'mode'), payload=self.ModesMapper.get_mapping(thermostat.mode)))
ret.append(MqttMessage(topic=self.format_topic(name, 'away'), payload=self.ModesMapper.away_mode_on_off(thermostat.mode)))
return ret
def device_for(self, mac):
return eq3bt.Thermostat(mac)
def format_topic(self, device, attr): def format_topic(self, device, attr):
return '/'.join([device, attr]) return '/'.join([device, attr])
+35 -23
View File
@@ -1,50 +1,62 @@
import importlib import importlib
import pip import pip
from apscheduler.schedulers.background import BackgroundScheduler
from interruptingcow import timeout from interruptingcow import timeout
from logger import _LOGGER
from workers_queue import _WORKERS_QUEUE
class WorkersManager: class WorkersManager:
def __init__(self, config): class Command:
self._config = config def __init__(self, callback, args=[], options={}):
self._interval_enabled_workers = {} self._callback = callback
self._workers = self._register_klasses() self._args = args
self._options = options
def update(self, worker_name): def execute(self):
messages = [] messages = []
with timeout(30): with timeout(30):
messages = self.workers[worker_name].status_update() messages = self._callback(*self._args)
for message in messages:
message.topic = '/'.join([self._config[worker_name]['topic_prefix'], message.topic])
_LOGGER.debug(messages)
return messages return messages
def __init__(self, config, mqtt):
self._mqtt = mqtt
self._workers = self._register_klasses(config)
@property @property
def workers(self): def workers(self):
return self._workers return self._workers
@property def _register_klasses(self, config):
def interval_enabled_workers(self):
return self._interval_enabled_workers
def _register_klasses(self):
arr = {} arr = {}
scheduler = BackgroundScheduler()
mqtt_callbacks = []
for worker_klass in self._config.keys(): for (worker_name, worker_config) in config.items():
moduleObj = importlib.import_module("workers.%s" % worker_klass) moduleObj = importlib.import_module("workers.%s" % worker_name)
klass = getattr(moduleObj, "%sWorker" % worker_klass.title()) klass = getattr(moduleObj, "%sWorker" % worker_name.title())
worker_obj = klass(**worker_config['args'])
if moduleObj.REQUIREMENTS is not None: if moduleObj.REQUIREMENTS is not None:
self._pip_install_helper(moduleObj.REQUIREMENTS) self._pip_install_helper(moduleObj.REQUIREMENTS)
if self._config[worker_klass]['update_interval'] is not None: if 'update_interval' in worker_config:
self._interval_enabled_workers[worker_klass] = self._config[worker_klass]['update_interval'] _LOGGER.debug("Added: %s with %d seconds interval" % (worker_name, worker_config['update_interval']))
_WORKERS_QUEUE.put(self.Command(worker_obj.status_update, []))
scheduler.add_job(lambda x: _WORKERS_QUEUE.put(self.Command(worker_obj.status_update, [])), 'interval', seconds=worker_config['update_interval'], args=[worker_name])
arr[worker_klass] = klass(**self._args_for(worker_klass)) if 'topic_subscription' in worker_config:
_LOGGER.debug("Subscribing to: %s" % worker_config['topic_subscription'] + '/#')
mqtt_callbacks.append((worker_config['topic_subscription'] + '/#', lambda client, _ , c: _WORKERS_QUEUE.put(self.Command(worker_obj.on_command, [c.topic, c.payload]))))
arr[worker_name] = worker_obj
scheduler.start()
self._mqtt.callbacks_subscription(mqtt_callbacks)
return arr return arr
def _args_for(self, klass):
return self._config[klass]['args']
def _pip_install_helper(self, package_names): def _pip_install_helper(self, package_names):
for package in package_names: for package in package_names:
pip.main(['install','-q', package]) pip.main(['install', '-q', package])
+4
View File
@@ -0,0 +1,4 @@
from queue import Queue
_WORKERS_QUEUE = Queue()