Support for commands handling
This commit is contained in:
+2
-1
@@ -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
@@ -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')
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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])
|
||||||
|
|||||||
+34
-22
@@ -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])
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
from queue import Queue
|
||||||
|
|
||||||
|
_WORKERS_QUEUE = Queue()
|
||||||
|
|
||||||
Reference in New Issue
Block a user