diff --git a/.gitignore b/.gitignore index 4dc4cec..6be934d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# Docker data +docker_config/nodered +docker_config/mqtt + # ---> Linux *~ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..aca24b3 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "geo"] + path = geo + url = https://git.mount-mockery.de/pylib/geo.git +[submodule "mqtt"] + path = mqtt + url = https://git.mount-mockery.de/pylib/mqtt.git +[submodule "report"] + path = report + url = https://git.mount-mockery.de/pylib/report.git diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..18d6947 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Verwendet IntelliSense zum Ermitteln möglicher Attribute. + // Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen. + // Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Main File execution", + "type": "python", + "request": "launch", + "program": "${workspaceFolder}/home_emulation.py", + "console": "integratedTerminal", + "justMyCode": true + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..211360c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,15 @@ +{ + "python.defaultInterpreterPath": "./venv/bin/python", + "autopep8.args": ["--max-line-length=150"], + "python.formatting.provider": "none", + "[python]": { + "editor.defaultFormatter": "ms-python.python", + "editor.formatOnSave": true + }, + "editor.formatOnSave": true, + "editor.fontSize": 14, + "emmet.includeLanguages": { "django-html": "html" }, + "python.testing.pytestArgs": ["-v", "--cov", "--cov-report=xml", "__test__"], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} diff --git a/config.py b/config.py new file mode 120000 index 0000000..0e51527 --- /dev/null +++ b/config.py @@ -0,0 +1 @@ +../smart_brain/config.py \ No newline at end of file diff --git a/devices/__init__.py b/devices/__init__.py new file mode 100644 index 0000000..c8b44c5 --- /dev/null +++ b/devices/__init__.py @@ -0,0 +1,23 @@ +import devices.base +import devices.livarno +import devices.shelly +import devices.tradfri + + +class null(devices.base.base): + """A dummy device for not yet existing devicetypes + + Args: + mqtt_client (mqtt.mqtt_client): A MQTT Client instance + topic (str): the base topic for this device + """ + def __init__(self, mqtt_client, topic, **kwargs): + super().__init__(mqtt_client, topic, **kwargs) + self.mqtt_client.add_callback(self.topic, self.__rx__) + self.mqtt_client.add_callback(self.topic + '/#', self.__rx__) + + def __rx__(self, client, userdata, message): + self.logger.warning("Got messaqge for device with missing implementation: topic=%s, payload=%s", message.topic, repr(message.payload)) + + def set_state(self, value): + self.logger.warning("Got set_state call for device with missing implementation.") diff --git a/devices/base.py b/devices/base.py new file mode 100644 index 0000000..85c37b9 --- /dev/null +++ b/devices/base.py @@ -0,0 +1,29 @@ +import logging + + +class base(dict): + """A base device for all devicetypes + + Args: + mqtt_client (mqtt.mqtt_client): A MQTT Client instance + topic (str): the base topic for this device + """ + PROPERTIES = [] + + def __init__(self, mqtt_client, topic, **kwargs): + super().__init__() + self.mqtt_client = mqtt_client + self.topic = topic + for key in kwargs: + setattr(self, key, kwargs[key]) + # + self.logger = logging.getLogger('devices') + for entry in self.topic.split('/'): + self.logger = self.logger.getChild(entry) + + def __set__(self, key, data): + if key in self.PROPERTIES: + self.logger.debug("Setting new property %s to %s", key, repr(data)) + self[key] = data + else: + self.logger.warning("Ignoring unsupported property %s", key) diff --git a/devices/livarno.py b/devices/livarno.py new file mode 100644 index 0000000..9138e83 --- /dev/null +++ b/devices/livarno.py @@ -0,0 +1,6 @@ +import devices.tradfri + + +class sw_br_ct(devices.tradfri.sw_br_ct): + def set_state(self, value): + self.__set__("state", "on" if value else "off") diff --git a/devices/shelly.py b/devices/shelly.py new file mode 100644 index 0000000..64f2373 --- /dev/null +++ b/devices/shelly.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +""" Communication (MQTT) + shelly + +- relay + | +- 0 ["on" / "off"] <- status + | | +- command ["on"/ "off"] <- command + | | +- energy [numeric] <- status + | +- 1 ["on" / "off"] <- status + | +- command ["on"/ "off"] <- command + | +- energy [numeric] <- status + +- input + | +- 0 [0 / 1] <- status + | +- 1 [0 / 1] <- status + +- input_event + | +- 0 <- status + | +- 1 <- status + +- logpush + | +- 0 [0 / 1] <- status + | +- 1 [0 / 1] <- status + +- temperature [numeric] °C <- status + +- temperature_f [numeric] F <- status + +- overtemperature [0 / 1] <- status + +- id <- status + +- model <- status + +- mac <- status + +- ip <- status + +- new_fw <- status + +- fw_ver <- status +""" + +from devices.base import base +import json + + +class sw_plain(base): + """A shelly device with switching functionality + + Args: + mqtt_client (mqtt.mqtt_client): A MQTT Client instance + topic (str): the base topic for this device + kwargs (**dict): cd_r0=list of devices connected to relay/0 + """ + PROPERTIES = [ + "relay/0", + ] + def __init__(self, mqtt_client, topic, **kwargs): + super().__init__(mqtt_client, topic, **kwargs) + if getattr(self, 'cd_r0', None) is None: + self.cd_r0 = [] + self["state"] = "off" + # + self.mqtt_client.add_callback(self.topic + '/relay/0/command', self.__rx_set__) + + def __rx_set__(self, client, userdata, message): + data = message.payload.decode('utf-8') + key = message.topic.split('/')[-3] + '/' + message.topic.split('/')[-2] + self.logger.info("Received set data for %s: %s", key, repr(data)) + self.__set__(key, data) + self.send_device_status(key) + if key == "relay/0": + for d in self.cd_r0: + d.set_state(data.lower() == "on") + + def send_device_status(self, key): + data = self[key] + self.logger.info("Sending status for %s: %s", key, repr(data)) + self.mqtt_client.send(self.topic + '/' + key, data) diff --git a/devices/tradfri.py b/devices/tradfri.py new file mode 100644 index 0000000..3d95073 --- /dev/null +++ b/devices/tradfri.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +""" +tradfri devices +=============== + +**Author:** + +* Dirk Alders + +**Description:** + + Emulation of tradfri devices + + Communication (MQTT) + + tradfri_light { + | "state": ["ON" / "OFF" / "TOGGLE"] + | "linkquality": [0...255] lqi + | "brightness": [0...254] + | "color_mode": ["color_temp"] + | "color_temp": ["coolest", "cool", "neutral", "warm", "warmest", 250...454] + | "color_temp_startup": ["coolest", "cool", "neutral", "warm", "warmest", "previous", 250...454] + | "update": [] + | } + +- get { + | "state": "" + | } + +- set { + "state": ["ON" / "OFF"] + "brightness": [0...256] + "color_temp": [250...454] + "transition": [0...] seconds + "brightness_move": [-X...0...X] X/s + "brightness_step": [-X...0...X] + "color_temp_move": [-X...0...X] X/s + "color_temp_step": [-X...0...X] + } +""" + +from devices.base import base +import json + + +class sw(base): + """A tradfri device with switching functionality + + Args: + mqtt_client (mqtt.mqtt_client): A MQTT Client instance + topic (str): the base topic for this device + kwargs (**dict): cd_st=list of devices connected to state + """ + PROPERTIES = [ + "state", + ] + def __init__(self, mqtt_client, topic, **kwargs): + super().__init__(mqtt_client, topic, **kwargs) + if getattr(self, 'cd_st', None) is None: + self.cd_st = [] + self["state"] = "off" + # + self.mqtt_client.add_callback(self.topic + '/set', self.__rx_set__) + self.mqtt_client.add_callback(self.topic + '/get', self.__rx_get__) + + def set_state(self, value): + self.__set__("state", "on" if value else "off") + self.send_device_status() + + def __rx_set__(self, client, userdata, message): + data = json.loads(message.payload) + self.logger.info("Received set data: %s", repr(data)) + for key in data: + self.__set__(key, data[key]) + self.send_device_status() + if "state" in data: + for d in self.cd_st: + d.set_state(data["state"].lower() == "on") + + def __rx_get__(self, client, userdata, message): + self.send_device_status() + + def send_device_status(self): + data = json.dumps(self) + self.logger.info("Sending status: %s", repr(data)) + self.mqtt_client.send(self.topic, data) + + +class sw_br(sw): + """A tradfri device with switching and brightness functionality + + Args: + mqtt_client (mqtt.mqtt_client): A MQTT Client instance + topic (str): the base topic for this device + """ + PROPERTIES = sw.PROPERTIES + [ + "brightness", + ] + def __init__(self, mqtt_client, topic, **kwargs): + super().__init__(mqtt_client, topic, **kwargs) + self["brightness"] = 64 + + +class sw_br_ct(sw_br): + """A tradfri device with switching, brightness and colortemp functionality + + Args: + mqtt_client (mqtt.mqtt_client): A MQTT Client instance + topic (str): the base topic for this device + """ + PROPERTIES = sw_br.PROPERTIES + [ + "color_temp", + ] + def __init__(self, mqtt_client, topic, **kwargs): + super().__init__(mqtt_client, topic, **kwargs) + self["color_temp"] = 413 diff --git a/docker_config/Makefile b/docker_config/Makefile new file mode 100644 index 0000000..1c1bd68 --- /dev/null +++ b/docker_config/Makefile @@ -0,0 +1,42 @@ +DOCKER_COMP = sudo docker-compose + +help: + echo Help is not yet implemented... + +nodered: + if [ ! -d nodered ]; then tar -xvzf nodered.tgz; fi +mqtt: + if [ ! -d mqtt ]; then tar -xvzf mqtt.tgz; fi +build: mqtt nodered + $(DOCKER_COMP) build + +up: mqtt nodered ## Starts the docker hub + $(DOCKER_COMP) up + +up_%: mqtt nodered ## Start a single service + $(DOCKER_COMP) up $(subst up_,,$@) + +down: ## Stops the docker hub + $(DOCKER_COMP) down --remove-orphans + +restart: ## Restarts the docker hub + $(DOCKER_COMP) restart + +status: ## Prompt Containers + $(DOCKER_COMP) ps + +sh_%: ## Connects to the application container + $(DOCKER_COMP) exec $(subst sh_,,$@) sh + +bash_%: ## Connects to the application container + $(DOCKER_COMP) exec $(subst bash_,,$@) bash + +logs: ## Displays the logs of the application container + $(DOCKER_COMP) logs -f + +logs_%: ## Displays the logs of the application container + $(DOCKER_COMP) logs -f $(subst logs_,,$@) + +cleanall: + rm -rf mqtt nodered + sudo docker system prune -a diff --git a/docker_config/docker-compose.yml b/docker_config/docker-compose.yml new file mode 100644 index 0000000..6af7014 --- /dev/null +++ b/docker_config/docker-compose.yml @@ -0,0 +1,21 @@ +version: '3' +services: + + my_nodered: + restart: always + image: nodered/node-red:latest + container_name: nodered + depends_on: + - mqtt + network_mode: host + volumes: + - ./nodered:/data + + mqtt: + build: mqtt + restart: always + ports: + - "1883:1883" + - "9001:9001" + volumes: + - ./mqtt:/mosquitto diff --git a/docker_config/mqtt.tgz b/docker_config/mqtt.tgz new file mode 100644 index 0000000..c124ecf Binary files /dev/null and b/docker_config/mqtt.tgz differ diff --git a/docker_config/nodered.tgz b/docker_config/nodered.tgz new file mode 100644 index 0000000..69401c8 Binary files /dev/null and b/docker_config/nodered.tgz differ diff --git a/geo b/geo new file mode 160000 index 0000000..11166bb --- /dev/null +++ b/geo @@ -0,0 +1 @@ +Subproject commit 11166bb27ad2335f7812fcb88c788397f5106751 diff --git a/home_emulation.py b/home_emulation.py new file mode 100644 index 0000000..4420405 --- /dev/null +++ b/home_emulation.py @@ -0,0 +1,100 @@ +import config +import devices +import logging +import mqtt +import report +import time + + +class device_creator(dict): + def __init__(self, mqtt_client): + self.mqtt_client = mqtt_client + # + # ground floor west + # floor + l1 = self.add_device(devices.livarno.sw_br_ct, config.TOPIC_GFW_FLOOR_MAIN_LIGHT_ZIGBEE % 1) + l2 = self.add_device(devices.livarno.sw_br_ct, config.TOPIC_GFW_FLOOR_MAIN_LIGHT_ZIGBEE % 2) + self.add_device(devices.shelly.sw_plain, config.TOPIC_GFW_FLOOR_MAIN_LIGHT_SHELLY, cd_r0=[l1, l2]) + + # marion + self.add_device(devices.shelly.sw_plain, config.TOPIC_GFW_MARION_MAIN_LIGHT_SHELLY) + self.add_device(devices.null, config.TOPIC_GFW_MARION_HEATING_VALVE_ZIGBEE) + + # dirk + l = self.add_device(devices.tradfri.sw_br_ct, config.TOPIC_GFW_DIRK_MAIN_LIGHT_ZIGBEE) + self.add_device(devices.shelly.sw_plain, config.TOPIC_GFW_DIRK_MAIN_LIGHT_SHELLY, cd_r0=[l]) + self.add_device(devices.null, config.TOPIC_GFW_DIRK_INPUT_DEVICE) + self.add_device(devices.null, config.TOPIC_GFW_DIRK_POWERPLUG) + self.add_device(devices.tradfri.sw_br_ct, config.TOPIC_GFW_DIRK_DESK_LIGHT_ZIGBEE) + self.add_device(devices.null, config.TOPIC_GFW_DIRK_HEATING_VALVE_ZIGBEE) + + # first floor west + # julian + l = self.add_device(devices.tradfri.sw_br_ct, config.TOPIC_FFW_JULIAN_MAIN_LIGHT_ZIGBEE) + self.add_device(devices.shelly.sw_plain, config.TOPIC_FFW_JULIAN_MAIN_LIGHT_SHELLY, cd_r0=[l]) + + # bath + self.add_device(devices.null, config.TOPIC_FFW_BATH_HEATING_VALVE_ZIGBEE) + + # livingroom + l = self.add_device(devices.tradfri.sw_br_ct, config.TOPIC_FFW_LIVINGROOM_MAIN_LIGHT_ZIGBEE) + self.add_device(devices.shelly.sw_plain, config.TOPIC_FFW_LIVINGROOM_MAIN_LIGHT_SHELLY, cd_r0=[l]) + + # sleep + l = self.add_device(devices.tradfri.sw_br, config.TOPIC_FFW_SLEEP_MAIN_LIGHT_ZIGBEE) + self.add_device(devices.shelly.sw_plain, config.TOPIC_FFW_SLEEP_MAIN_LIGHT_SHELLY, cd_r0=[l]) + + + # first floor east + # floor + self.add_device(devices.shelly.sw_plain, config.TOPIC_FFE_FLOOR_MAIN_LIGHT_SHELLY) + + # kitchen + self.add_device(devices.shelly.sw_plain, config.TOPIC_FFE_KITCHEN_MAIN_LIGHT_SHELLY) + self.add_device(devices.null, config.TOPIC_FFE_KITCHEN_CIRCULATION_PUMP_SHELLY) + + # diningroom + self.add_device(devices.shelly.sw_plain, config.TOPIC_FFE_DININGROOM_MAIN_LIGHT_SHELLY) + self.add_device(devices.null, config.TOPIC_FFE_DININGROOM_FLOOR_LAMP_POWERPLUG) + self.add_device(devices.null, config.TOPIC_FFE_DININGROOM_GARLAND_POWERPLUG) + + # sleep + l = self.add_device(devices.tradfri.sw_br_ct, config.TOPIC_FFE_SLEEP_MAIN_LIGHT_ZIGBEE) + self.add_device(devices.shelly.sw_plain, config.TOPIC_FFE_SLEEP_MAIN_LIGHT_SHELLY, cd_r0=[l]) + self.add_device(devices.null, config.TOPIC_FFE_SLEEP_INPUT_DEVICE) + self.add_device(devices.tradfri.sw_br, config.TOPIC_FFE_SLEEP_BED_LIGHT_DI_ZIGBEE) + self.add_device(devices.null, config.TOPIC_FFE_SLEEP_BED_LIGHT_MA_POWERPLUG) + self.add_device(devices.null, config.TOPIC_FFE_SLEEP_HEATING_VALVE_ZIGBEE) + + # livingroom + l = self.add_device(devices.tradfri.sw_br_ct, config.TOPIC_FFE_LIVINGROOM_MAIN_LIGHT_ZIGBEE) + self.add_device(devices.shelly.sw_plain, config.TOPIC_FFE_LIVINGROOM_MAIN_LIGHT_SHELLY, cd_r0=[l]) + for i in range(1,7): + self.add_device(devices.tradfri.sw_br_ct, config.TOPIC_FFE_LIVINGROOM_FLOOR_LAMP_ZIGBEE % i) + self.add_device(devices.null, config.TOPIC_FFE_LIVINGROOM_XMAS_TREE_POWERPLUG) + self.add_device(devices.null, config.TOPIC_FFE_LIVINGROOM_XMAS_STAR_POWERPLUG) + + + # first floor east + # floor + self.add_device(devices.shelly.sw_plain, config.TOPIC_STW_STAIRWAY_MAIN_LIGHT_SHELLY) + self.add_device(devices.null, config.TOPIC_STW_STAIRWAY_MAIN_LIGHT_MOTION_SENSOR_FF) + self.add_device(devices.null, config.TOPIC_STW_STAIRWAY_MAIN_LIGHT_MOTION_SENSOR_GF) + + def add_device(self, deviceclass, topic, **kwargs): + self[topic] = deviceclass(self.mqtt_client, topic, **kwargs) + return self[topic] + +if __name__ == "__main__": + report.stdoutLoggingConfigure(( + (config.APP_NAME, logging.DEBUG), + ('devices', logging.DEBUG), + ), report.SHORT_FMT) + + mc = mqtt.mqtt_client(host=config.MQTT_SERVER, port=config.MQTT_PORT, username=config.MQTT_USER, + password=config.MQTT_PASSWORD, name='home_emulation') + + device_dict = device_creator(mc) + + while (True): + time.sleep(1) diff --git a/mqtt b/mqtt new file mode 160000 index 0000000..1adfb06 --- /dev/null +++ b/mqtt @@ -0,0 +1 @@ +Subproject commit 1adfb0626e7777c6d29be65d4ad4ce2d57541301 diff --git a/report b/report new file mode 160000 index 0000000..b53dd30 --- /dev/null +++ b/report @@ -0,0 +1 @@ +Subproject commit b53dd30eae1d679b7eec4999bec50aed55bc105b