瀏覽代碼

First functional version

master
Dirk Alders 1 周之前
父節點
當前提交
f4c8eebc98
共有 19 個文件被更改,包括 1423 次插入0 次删除
  1. 15
    0
      .gitmodules
  2. 16
    0
      .vscode/launch.json
  3. 14
    0
      .vscode/settings.json
  4. 138
    0
      base.py
  5. 14
    0
      config.py
  6. 103
    0
      devices/__init__.py
  7. 134
    0
      devices/base.py
  8. 119
    0
      devices/brennenstuhl.py
  9. 262
    0
      devices/mydevices.py
  10. 171
    0
      devices/shelly.py
  11. 107
    0
      devices/silvercrest.py
  12. 192
    0
      devices/tradfri.py
  13. 1
    0
      geo
  14. 1
    0
      mqtt
  15. 1
    0
      report
  16. 129
    0
      smart_enlife.py
  17. 4
    0
      smart_enlife.sh
  18. 1
    0
      state_machine
  19. 1
    0
      task

+ 15
- 0
.gitmodules 查看文件

@@ -0,0 +1,15 @@
1
+[submodule "mqtt"]
2
+	path = mqtt
3
+	url = https://git.mount-mockery.de/pylib/mqtt.git
4
+[submodule "report"]
5
+	path = report
6
+	url = https://git.mount-mockery.de/pylib/report.git
7
+[submodule "geo"]
8
+	path = geo
9
+	url = https://git.mount-mockery.de/pylib/geo.git
10
+[submodule "task"]
11
+	path = task
12
+	url = https://git.mount-mockery.de/pylib/task.git
13
+[submodule "state_machine"]
14
+	path = state_machine
15
+	url = https://git.mount-mockery.de/pylib/state_machine.git

+ 16
- 0
.vscode/launch.json 查看文件

@@ -0,0 +1,16 @@
1
+{
2
+    // Verwendet IntelliSense zum Ermitteln möglicher Attribute.
3
+    // Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen.
4
+    // Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387
5
+    "version": "0.2.0",
6
+    "configurations": [
7
+        {
8
+            "name": "Python: Main File execution",
9
+            "type": "debugpy",
10
+            "request": "launch",
11
+            "program": "${workspaceFolder}/smart_enlife.py",
12
+            "console": "integratedTerminal",
13
+            "justMyCode": true
14
+        }
15
+    ]
16
+}

+ 14
- 0
.vscode/settings.json 查看文件

@@ -0,0 +1,14 @@
1
+{
2
+  "python.defaultInterpreterPath": "./venv/bin/python",
3
+  "autopep8.args": ["--max-line-length=150"],
4
+  "[python]": {
5
+    "python.formatting.provider": "none",
6
+    "editor.defaultFormatter": "ms-python.autopep8",
7
+    "editor.formatOnSave": true
8
+  },
9
+  "editor.fontSize": 14,
10
+  "emmet.includeLanguages": { "django-html": "html" },
11
+  "python.testing.pytestArgs": ["-v", "--cov", "--cov-report=xml", "__test__"],
12
+  "python.testing.unittestEnabled": false,
13
+  "python.testing.pytestEnabled": true
14
+}

+ 138
- 0
base.py 查看文件

@@ -0,0 +1,138 @@
1
+import json
2
+import logging
3
+import task
4
+
5
+try:
6
+    from config import APP_NAME as ROOT_LOGGER_NAME
7
+except ImportError:
8
+    ROOT_LOGGER_NAME = 'root'
9
+
10
+
11
+class common_base(dict):
12
+    DEFAULT_VALUES = {}
13
+
14
+    def __init__(self, default_values=None):
15
+        super().__init__(default_values or self.DEFAULT_VALUES)
16
+        #
17
+        self.__callback_list__ = []
18
+        self.logger = logging.getLogger(ROOT_LOGGER_NAME).getChild("devices")
19
+
20
+    def add_callback(self, key, data, callback, on_change_only=False, init_now=False):
21
+        """
22
+        key: key or None for all keys
23
+        data: data or None for all data
24
+        """
25
+        cb_tup = (key, data, callback, on_change_only)
26
+        if cb_tup not in self.__callback_list__:
27
+            self.__callback_list__.append(cb_tup)
28
+        if init_now and self.get(key) is not None:
29
+            callback(self, key, self[key])
30
+
31
+    def set(self, key, data, block_callback=[]):
32
+        if key in self.keys():
33
+            value_changed = self[key] != data
34
+            self[key] = data
35
+            for cb_key, cb_data, callback, on_change_only in self.__callback_list__:
36
+                if cb_key is None or key == cb_key:                 # key fits to callback definition
37
+                    if cb_data is None or cb_data == self[key]:     # data fits to callback definition
38
+                        if value_changed or not on_change_only:     # change status fits to callback definition
39
+                            if not callback in block_callback:      # block given callbacks
40
+                                callback(self, key, self[key])
41
+        else:
42
+            self.logger.warning("Unexpected key %s", key)
43
+
44
+
45
+class mqtt_base(common_base):
46
+    def __init__(self, mqtt_client, topic, default_values=None):
47
+        super().__init__(default_values)
48
+        #
49
+        self.mqtt_client = mqtt_client
50
+        self.topic = topic
51
+        for entry in self.topic.split('/'):
52
+            self.logger = self.logger.getChild(entry)
53
+
54
+
55
+class videv_base(mqtt_base):
56
+    KEY_INFO = '__info__'
57
+    #
58
+    SET_TOPIC = "set"
59
+
60
+    def __init__(self, mqtt_client, topic, default_values=None):
61
+        super().__init__(mqtt_client, topic, default_values=default_values)
62
+        self.__display_dict__ = {}
63
+        self.__control_dict__ = {}
64
+        self.__periodic__ = task.periodic(300, self.send_all)
65
+        self.__periodic__.run()
66
+
67
+    def send_all(self, rt):
68
+        try:
69
+            for key in self:
70
+                if self[key] is not None:
71
+                    self.__tx__(key, self[key])
72
+        except RuntimeError:
73
+            self.logger.warning("Runtimeerror while sending cyclic videv information. This may happen on startup.")
74
+
75
+    def add_display(self, my_key, ext_device, ext_key, on_change_only=True):
76
+        """
77
+        listen to data changes of ext_device and update videv information
78
+        """
79
+        if my_key not in self.keys():
80
+            self[my_key] = None
81
+        if ext_device.__class__.__name__ == "group":
82
+            # store information to identify callback from ext_device
83
+            self.__display_dict__[(id(ext_device[0]), ext_key)] = my_key
84
+            # register a callback to listen for data from external device
85
+            ext_device[0].add_callback(ext_key, None, self.__rx_ext_device_data__, on_change_only, init_now=True)
86
+        else:
87
+            # store information to identify callback from ext_device
88
+            self.__display_dict__[(id(ext_device), ext_key)] = my_key
89
+            # register a callback to listen for data from external device
90
+            ext_device.add_callback(ext_key, None, self.__rx_ext_device_data__, on_change_only, init_now=True)
91
+        # send initial display data to videv interface
92
+        data = ext_device.get(ext_key)
93
+        if data is not None:
94
+            self.__tx__(my_key, data)
95
+
96
+    def __rx_ext_device_data__(self, ext_device, ext_key, data):
97
+        my_key = self.__display_dict__[(id(ext_device), ext_key)]
98
+        self.set(my_key, data)
99
+        self.__tx__(my_key, data)
100
+
101
+    def __tx__(self, key, data):
102
+        if type(data) not in (str, ):
103
+            data = json.dumps(data)
104
+        self.mqtt_client.send('/'.join([self.topic, key]), data)
105
+
106
+    def add_control(self, my_key, ext_device, ext_key, on_change_only=True):
107
+        """
108
+        listen to videv information and pass data to ext_device
109
+        """
110
+        if my_key not in self.keys():
111
+            self[my_key] = None
112
+        # store information to identify callback from videv
113
+        self.__control_dict__[my_key] = (ext_device, ext_key, on_change_only)
114
+        # add callback for videv changes
115
+        self.mqtt_client.add_callback('/'.join([self.topic, my_key, self.SET_TOPIC]), self.__rx_videv_data__)
116
+
117
+    def __rx_videv_data__(self, client, userdata, message):
118
+        my_key = message.topic.split('/')[-2]
119
+        try:
120
+            data = json.loads(message.payload)
121
+        except json.decoder.JSONDecodeError:
122
+            data = message.payload
123
+        ext_device, ext_key, on_change_only = self.__control_dict__[my_key]
124
+        if my_key in self.keys():
125
+            if data != self[my_key] or not on_change_only:
126
+                ext_device.send_command(ext_key, data)
127
+        else:
128
+            self.logger.info("Ignoring rx message with topic %s", message.topic)
129
+
130
+    def add_routing(self, my_key, ext_device, ext_key, on_change_only_disp=True, on_change_only_videv=True):
131
+        """
132
+        listen to data changes of ext_device and update videv information
133
+        and
134
+        listen to videv information and pass data to ext_device
135
+        """
136
+        # add display
137
+        self.add_display(my_key, ext_device, ext_key, on_change_only_disp)
138
+        self.add_control(my_key, ext_device, ext_key, on_change_only_videv)

+ 14
- 0
config.py 查看文件

@@ -0,0 +1,14 @@
1
+import geo
2
+import logging
3
+
4
+DEBUG = False   # False: logging to stdout with given LOGLEVEL  - True: logging all to localhost:19996 and warnings or higher to stdout
5
+LOGLEVEL = logging.DEBUG  # INFO
6
+
7
+GEO_POSITION = geo.gps.coordinate(lat=49.519167, lon=9.3672222)
8
+
9
+APP_NAME = "smart_enlife"
10
+
11
+MQTT_SERVER = "mqtt.home"
12
+MQTT_PORT = 1883
13
+MQTT_USER = "smarthome"
14
+MQTT_PASSWORD = ""

+ 103
- 0
devices/__init__.py 查看文件

@@ -0,0 +1,103 @@
1
+#!/usr/bin/env python
2
+# -*- coding: utf-8 -*-
3
+#
4
+"""
5
+devices (DEVICES)
6
+=================
7
+
8
+**Author:**
9
+
10
+* Dirk Alders <sudo-dirk@mount-mockery.de>
11
+
12
+**Description:**
13
+
14
+    This Module supports smarthome devices
15
+
16
+**Submodules:**
17
+
18
+* :mod:`shelly`
19
+* :mod:`silvercrest_powerplug`
20
+
21
+**Unittest:**
22
+
23
+        See also the :download:`unittest <devices/_testresults_/unittest.pdf>` documentation.
24
+
25
+**Module Documentation:**
26
+
27
+"""
28
+
29
+try:
30
+    from config import APP_NAME as ROOT_LOGGER_NAME
31
+except ImportError:
32
+    ROOT_LOGGER_NAME = 'root'
33
+
34
+from devices.shelly import shelly as shelly_sw1
35
+from devices.tradfri import tradfri_light as tradfri_sw
36
+from devices.tradfri import tradfri_light as tradfri_sw_br
37
+from devices.tradfri import tradfri_light as tradfri_sw_br_ct
38
+from devices.tradfri import tradfri_button as tradfri_button
39
+from devices.tradfri import tradfri_light as livarno_sw_br_ct
40
+from devices.brennenstuhl import brennenstuhl_heatingvalve
41
+from devices.silvercrest import silvercrest_powerplug
42
+from devices.silvercrest import silvercrest_motion_sensor
43
+from devices.mydevices import powerplug as my_powerplug
44
+from devices.mydevices import audio_status
45
+from devices.mydevices import remote
46
+my_ambient = None
47
+
48
+
49
+class group(object):
50
+    def __init__(self, *args):
51
+        super().__init__()
52
+        self._members = args
53
+        self._iter_counter = 0
54
+        #
55
+        self.methods = []
56
+        self.variables = []
57
+        for name in [m for m in args[0].__class__.__dict__.keys()]:
58
+            if not name.startswith('_') and callable(getattr(args[0], name)):   # add all public callable attributes to the list
59
+                self.methods.append(name)
60
+            if not name.startswith('_') and not callable(getattr(args[0], name)):   # add all public callable attributes to the list
61
+                self.variables.append(name)
62
+        #
63
+        for member in self:
64
+            methods = [m for m in member.__class__.__dict__.keys() if not m.startswith(
65
+                '_') if not m.startswith('_') and callable(getattr(args[0], m))]
66
+            if self.methods != methods:
67
+                raise ValueError("All given instances needs to have same methods:", self.methods, methods)
68
+            #
69
+            variables = [v for v in member.__class__.__dict__.keys() if not v.startswith(
70
+                '_') if not v.startswith('_') and not callable(getattr(args[0], v))]
71
+            if self.variables != variables:
72
+                raise ValueError("All given instances needs to have same variables:", self.variables, variables)
73
+
74
+    def __iter__(self):
75
+        return self
76
+
77
+    def __next__(self):
78
+        if self._iter_counter < len(self):
79
+            self._iter_counter += 1
80
+            return self._members[self._iter_counter - 1]
81
+        self._iter_counter = 0
82
+        raise StopIteration
83
+
84
+    def __getitem__(self, i):
85
+        return self._members[i]
86
+
87
+    def __len__(self):
88
+        return len(self._members)
89
+
90
+    def __getattribute__(self, name):
91
+        def group_execution(*args, **kwargs):
92
+            for member in self[:]:
93
+                m = getattr(member, name)
94
+                m(*args, **kwargs)
95
+        try:
96
+            rv = super().__getattribute__(name)
97
+        except AttributeError:
98
+            if callable(getattr(self[0], name)):
99
+                return group_execution
100
+            else:
101
+                return getattr(self[0], name)
102
+        else:
103
+            return rv

+ 134
- 0
devices/base.py 查看文件

@@ -0,0 +1,134 @@
1
+#!/usr/bin/env python
2
+# -*- coding: utf-8 -*-
3
+#
4
+from base import mqtt_base
5
+from base import videv_base
6
+import json
7
+import time
8
+
9
+
10
+def is_json(data):
11
+    try:
12
+        json.loads(data)
13
+    except json.decoder.JSONDecodeError:
14
+        return False
15
+    else:
16
+        return True
17
+
18
+
19
+class base(mqtt_base):
20
+    TX_TOPIC = "set"
21
+    TX_VALUE = 0
22
+    TX_DICT = 1
23
+    TX_TYPE = -1
24
+    TX_FILTER_DATA_KEYS = []
25
+    #
26
+    RX_KEYS = []
27
+    RX_IGNORE_TOPICS = []
28
+    RX_IGNORE_KEYS = []
29
+    RX_FILTER_DATA_KEYS = []
30
+    #
31
+    CFG_DATA = {}
32
+
33
+    def __init__(self, mqtt_client, topic):
34
+        super().__init__(mqtt_client, topic, default_values=dict.fromkeys(self.RX_KEYS))
35
+        # data storage
36
+        self.__cfg_by_mid__ = None
37
+        # initialisations
38
+        mqtt_client.add_callback(topic=self.topic, callback=self.receive_callback)
39
+        mqtt_client.add_callback(topic=self.topic+"/#", callback=self.receive_callback)
40
+        #
41
+        self.add_callback(None, None, self.__state_logging__, on_change_only=True)
42
+
43
+    def __cfg_callback__(self, key, value, mid):
44
+        if self.CFG_DATA.get(key) != value and self.__cfg_by_mid__ != mid and mid is not None:
45
+            self.__cfg_by_mid__ = mid
46
+            self.logger.warning("Differing configuration identified: Sending default configuration to defice: %s", repr(self.CFG_DATA))
47
+            if self.TX_TYPE == self.TX_DICT:
48
+                self.mqtt_client.send(self.topic + '/' + self.TX_TOPIC, json.dumps(self.CFG_DATA))
49
+            else:
50
+                for key in self.CFG_DATA:
51
+                    self.send_command(key, self.CFG_DATA.get(key))
52
+
53
+    def set(self, key, data, mid=None, block_callback=[]):
54
+        if key in self.CFG_DATA:
55
+            self.__cfg_callback__(key, data, mid)
56
+        if key in self.RX_IGNORE_KEYS:
57
+            pass    # ignore these keys
58
+        elif key in self.RX_KEYS:
59
+            return super().set(key, data, block_callback)
60
+        else:
61
+            self.logger.warning("Unexpected key %s", key)
62
+
63
+    def receive_callback(self, client, userdata, message):
64
+        if message.topic != self.topic + '/' + videv_base.KEY_INFO:
65
+            content_key = message.topic[len(self.topic) + 1:]
66
+            if content_key not in self.RX_IGNORE_TOPICS and (not message.topic.endswith(self.TX_TOPIC) or len(self.TX_TOPIC) == 0):
67
+                self.logger.debug("Unpacking content_key \"%s\" from message.", content_key)
68
+                if is_json(message.payload):
69
+                    data = json.loads(message.payload)
70
+                    if type(data) is dict:
71
+                        for key in data:
72
+                            self.set(key, self.__device_to_instance_filter__(key, data[key]), message.mid)
73
+                    else:
74
+                        self.set(content_key, self.__device_to_instance_filter__(content_key, data), message.mid)
75
+                # String
76
+                else:
77
+                    self.set(content_key, self.__device_to_instance_filter__(content_key, message.payload.decode('utf-8')), message.mid)
78
+            else:
79
+                self.logger.debug("Ignoring topic %s", content_key)
80
+
81
+    def __device_to_instance_filter__(self, key, data):
82
+        if key in self.RX_FILTER_DATA_KEYS:
83
+            if data in [1, 'on', 'ON']:
84
+                return True
85
+            elif data in [0, 'off', 'OFF']:
86
+                return False
87
+        return data
88
+
89
+    def __instance_to_device_filter__(self, key, data):
90
+        if key in self.TX_FILTER_DATA_KEYS:
91
+            if data is True:
92
+                return "on"
93
+            elif data is False:
94
+                return "off"
95
+        return data
96
+
97
+    def send_command(self, key, data):
98
+        data = self.__instance_to_device_filter__(key, data)
99
+        if self.TX_TOPIC is not None:
100
+            if self.TX_TYPE < 0:
101
+                self.logger.error("Unknown tx type. Set TX_TYPE of class to a known value")
102
+            else:
103
+                self.logger.debug("Sending data for %s - %s", key, str(data))
104
+                if self.TX_TYPE == self.TX_DICT:
105
+                    try:
106
+                        self.mqtt_client.send('/'.join([self.topic, self.TX_TOPIC]), json.dumps({key: data}))
107
+                    except TypeError:
108
+                        print(self.topic)
109
+                        print(key.__dict__)
110
+                        print(key)
111
+                        print(data)
112
+                        raise TypeError
113
+                else:
114
+                    if type(data) not in [str, bytes]:
115
+                        data = json.dumps(data)
116
+                    self.mqtt_client.send('/'.join([self.topic, key, self.TX_TOPIC] if len(self.TX_TOPIC) > 0 else [self.topic, key]), data)
117
+        else:
118
+            self.logger.error("Unknown tx toptic. Set TX_TOPIC of class to a known value")
119
+
120
+
121
+class base_output(base):
122
+    def __init__(self, mqtt_client, topic):
123
+        super().__init__(mqtt_client, topic)
124
+        self.__all_off_enabled__ = True
125
+
126
+    def disable_all_off(self, state=True):
127
+        self.__all_off_enabled__ = not state
128
+
129
+    def all_off(self):
130
+        if self.__all_off_enabled__:
131
+            try:
132
+                self.__all_off__()
133
+            except (AttributeError, TypeError) as e:
134
+                self.logger.warning("Method all_off was used, but __all_off__ method wasn't callable: %s", repr(e))

+ 119
- 0
devices/brennenstuhl.py 查看文件

@@ -0,0 +1,119 @@
1
+#!/usr/bin/env python
2
+# -*- coding: utf-8 -*-
3
+#
4
+from devices.base import base
5
+import json
6
+import task
7
+import time
8
+
9
+
10
+class brennenstuhl_heatingvalve(base):
11
+    """ Communication (MQTT)
12
+
13
+        brennenstuhl_heatingvalve {
14
+                                |      "away_mode": ["ON", "OFF"]
15
+                                |      "battery": [0...100] %
16
+                                |      "child_lock": ["LOCK", "UNLOCK"]
17
+                                |      "current_heating_setpoint": [5...30] °C
18
+                                |      "linkquality": [0...255] lqi
19
+                                |      "local_temperature": [numeric] °C
20
+                                |      "preset": ["manual", ...]
21
+                                |      "system_mode": ["heat", ...]
22
+                                |      "valve_detection": ["ON", "OFF"]
23
+                                |      "window_detection": ["ON", "OFF"]
24
+                                | }
25
+                                +- set {
26
+                                           "away_mode": ["ON", "OFF", "TOGGLE"]
27
+                                           "child_lock": ["LOCK", "UNLOCK"]
28
+                                           "current_heating_setpoint": [5...30] °C
29
+                                           "preset": ["manual", ...]
30
+                                           "system_mode": ["heat", ...]
31
+                                           "valve_detection": ["ON", "OFF", "TOGGLE"]
32
+                                           "window_detection": ["ON", "OFF", "TOGGLE"]
33
+                                       }
34
+    """
35
+    KEY_LINKQUALITY = "linkquality"
36
+    KEY_BATTERY = "battery"
37
+    KEY_HEATING_SETPOINT = "current_heating_setpoint"
38
+    KEY_TEMPERATURE = "local_temperature"
39
+    #
40
+    KEY_AWAY_MODE = "away_mode"
41
+    KEY_CHILD_LOCK = "child_lock"
42
+    KEY_PRESET = "preset"
43
+    KEY_SYSTEM_MODE = "system_mode"
44
+    KEY_VALVE_DETECTION = "valve_detection"
45
+    KEY_WINDOW_DETECTION = "window_detection"
46
+    #
47
+    RETRY_CYCLE_TIME = 2.5
48
+    MAX_TX_RETRIES = 20
49
+    RETRY_TIMEOUT = RETRY_CYCLE_TIME * MAX_TX_RETRIES
50
+    #
51
+    TX_TYPE = base.TX_DICT
52
+    #
53
+    RX_KEYS = [KEY_LINKQUALITY, KEY_BATTERY, KEY_HEATING_SETPOINT, KEY_TEMPERATURE]
54
+    RX_IGNORE_KEYS = [KEY_AWAY_MODE, KEY_CHILD_LOCK, KEY_PRESET, KEY_SYSTEM_MODE, KEY_VALVE_DETECTION, KEY_WINDOW_DETECTION]
55
+    #
56
+    CFG_DATA = {
57
+        KEY_WINDOW_DETECTION: "ON",
58
+        KEY_VALVE_DETECTION: "ON",
59
+        KEY_SYSTEM_MODE: "heat",
60
+        KEY_PRESET: "manual"
61
+    }
62
+
63
+    def __init__(self, mqtt_client, topic):
64
+        super().__init__(mqtt_client, topic)
65
+        self.add_callback(self.KEY_HEATING_SETPOINT, None, self.__valave_temp_rx__)
66
+        self.__tx_temperature__ = None
67
+        self.__rx_temperature__ = None
68
+        self.__tx_timestamp__ = 0
69
+        #
70
+        self.task = task.periodic(self.RETRY_CYCLE_TIME, self.__task__)
71
+        self.task.run()
72
+
73
+    def __state_logging__(self, inst, key, data):
74
+        if key in [self.KEY_HEATING_SETPOINT, self.KEY_CHILD_LOCK, self.KEY_WINDOW_DETECTION, self.KEY_VALVE_DETECTION]:
75
+            self.logger.info("State change of '%s' to '%s'", key, repr(data))
76
+
77
+    def send_command(self, key, data):
78
+        if key == self.KEY_HEATING_SETPOINT:
79
+            self.__tx_temperature__ = data
80
+            self.__tx_timestamp__ = time.time()
81
+        base.send_command(self, key, data)
82
+
83
+    def __valave_temp_rx__(self, inst, key, data):
84
+        if key == self.KEY_HEATING_SETPOINT:
85
+            self.__rx_temperature__ = data
86
+
87
+    def __task__(self, rt):
88
+        if self.__tx_temperature__ is not None and self.__tx_timestamp__ is not None:   # Already send a setpoint
89
+            if self.__tx_temperature__ != self.__rx_temperature__:                      # Setpoint and valve feedback are unequal
90
+                if time.time() - self.__tx_timestamp__ < self.RETRY_TIMEOUT:            # Timeout condition allows resend of setpoint
91
+                    self.logger.warning("Setpoint not yet acknoledged by device. Sending setpoint again")
92
+                    self.set_heating_setpoint(self.__tx_temperature__)
93
+                    return
94
+            else:
95
+                self.__tx_timestamp__ = None                                            # Disable resend logic, if setpoint and valve setpoint are equal
96
+
97
+    #
98
+    # RX
99
+    #
100
+    @property
101
+    def linkqulity(self):
102
+        return self.get(self.KEY_LINKQUALITY)
103
+
104
+    @property
105
+    def heating_setpoint(self):
106
+        return self.get(self.KEY_HEATING_SETPOINT)
107
+
108
+    @property
109
+    def temperature(self):
110
+        return self.get(self.KEY_TEMPERATURE)
111
+
112
+    #
113
+    # TX
114
+    #
115
+    def set_heating_setpoint(self, setpoint):
116
+        self.send_command(self.KEY_HEATING_SETPOINT, setpoint)
117
+
118
+    def set_heating_setpoint_mcb(self, device, key, data):
119
+        self.set_heating_setpoint(data)

+ 262
- 0
devices/mydevices.py 查看文件

@@ -0,0 +1,262 @@
1
+#!/usr/bin/env python
2
+# -*- coding: utf-8 -*-
3
+#
4
+from devices.base import base, base_output
5
+import logging
6
+
7
+
8
+class powerplug(base_output):
9
+    """ Communication (MQTT)
10
+
11
+        my_powerplug
12
+                   +- output
13
+                           +- 1 [True, False]                   <- status
14
+                           |  +- set [True, False, "toggle"]    <- command
15
+                           +- 2 [True, False]                   <- status
16
+                           |  +- set [True, False, "toggle"]    <- command
17
+                           +- 3 [True, False]                   <- status
18
+                           |  +- set [True, False, "toggle"]    <- command
19
+                           +- 4 [True, False]                   <- status
20
+                           |  +- set [True, False, "toggle"]    <- command
21
+                           +- all
22
+                              +- set [True, False, "toggle"]    <- command
23
+    """
24
+    KEY_OUTPUT_0 = "output/1"
25
+    KEY_OUTPUT_1 = "output/2"
26
+    KEY_OUTPUT_2 = "output/3"
27
+    KEY_OUTPUT_3 = "output/4"
28
+    KEY_OUTPUT_ALL = "output/all"
29
+    KEY_OUTPUT_LIST = [KEY_OUTPUT_0, KEY_OUTPUT_1, KEY_OUTPUT_2, KEY_OUTPUT_3]
30
+    #
31
+    TX_TYPE = base.TX_VALUE
32
+    #
33
+    RX_KEYS = [KEY_OUTPUT_0, KEY_OUTPUT_1, KEY_OUTPUT_2, KEY_OUTPUT_3]
34
+
35
+    def __state_logging__(self, inst, key, data):
36
+        if key in self.KEY_OUTPUT_LIST:
37
+            self.logger.info("State change of '%s' to '%s'", key, repr(data))
38
+
39
+    #
40
+    # RX
41
+    #
42
+    @property
43
+    def output_0(self):
44
+        """rv: [True, False]"""
45
+        return self.get(self.KEY_OUTPUT_0)
46
+
47
+    @property
48
+    def output_1(self):
49
+        """rv: [True, False]"""
50
+        return self.get(self.KEY_OUTPUT_1)
51
+
52
+    @property
53
+    def output_2(self):
54
+        """rv: [True, False]"""
55
+        return self.get(self.KEY_OUTPUT_2)
56
+
57
+    @property
58
+    def output_3(self):
59
+        """rv: [True, False]"""
60
+        return self.get(self.KEY_OUTPUT_3)
61
+
62
+    #
63
+    # TX
64
+    #
65
+    def set_output(self, key, state):
66
+        if key in self.KEY_OUTPUT_LIST:
67
+            self.send_command(key, state)
68
+        else:
69
+            logging.error("Unknown key to set the output!")
70
+
71
+    def set_output_0(self, state):
72
+        """state: [True, False]"""
73
+        self.send_command(self.KEY_OUTPUT_0, state)
74
+
75
+    def set_output_0_mcb(self, device, key, data):
76
+        self.set_output_0(data)
77
+
78
+    def toggle_output_0_mcb(self, device, key, data):
79
+        self.set_output_0(not self.output_0)
80
+
81
+    def set_output_1(self, state):
82
+        """state: [True, False]"""
83
+        self.send_command(self.KEY_OUTPUT_1, state)
84
+
85
+    def set_output_1_mcb(self, device, key, data):
86
+        self.set_output_1(data)
87
+
88
+    def toggle_output_1_mcb(self, device, key, data):
89
+        self.set_output_1(not self.output_1)
90
+
91
+    def set_output_2(self, state):
92
+        """state: [True, False]"""
93
+        self.send_command(self.KEY_OUTPUT_2, state)
94
+
95
+    def set_output_2_mcb(self, device, key, data):
96
+        self.set_output_2(data)
97
+
98
+    def toggle_output_2_mcb(self, device, key, data):
99
+        self.set_output_2(not self.output_2)
100
+
101
+    def set_output_3(self, state):
102
+        """state: [True, False]"""
103
+        self.send_command(self.KEY_OUTPUT_3, state)
104
+
105
+    def set_output_3_mcb(self, device, key, data):
106
+        self.set_output_3(data)
107
+
108
+    def toggle_output_3_mcb(self, device, key, data):
109
+        self.set_output_3(not self.output_3)
110
+
111
+    def set_output_all(self, state):
112
+        """state: [True, False, 'toggle']"""
113
+        self.send_command(self.KEY_OUTPUT_ALL, state)
114
+
115
+    def set_output_all_mcb(self, device, key, data):
116
+        self.set_output_all(data)
117
+
118
+    def __all_off__(self):
119
+        self.set_output_all(False)
120
+
121
+
122
+class remote(base):
123
+    """ Communication (MQTT)
124
+
125
+        remote (RAS5)                               <- command
126
+             +- CD [dc]
127
+             +- LINE1 [dc]
128
+             +- LINE2 [dc]
129
+             +- LINE3 [dc]
130
+             +- MUTE [dc]
131
+             +- POWER [dc]
132
+             +- VOLDOWN [dc]
133
+             +- VOLUP [dc]
134
+             +- PHONO [dc]
135
+             +- DOCK [dc]
136
+
137
+        remote (EUR642100)                          <- command
138
+             +- OPEN_CLOSE [dc]
139
+             +- VOLDOWN [dc]
140
+             +- VOLUP [dc]
141
+             +- ONE [dc]
142
+             +- TWO [dc]
143
+             +- THREE [dc]
144
+             +- FOUR [dc]
145
+             +- FIVE [dc]
146
+             +- SIX [dc]
147
+             +- SEVEN [dc]
148
+             +- EIGHT [dc]
149
+             +- NINE [dc]
150
+             +- ZERO [dc]
151
+             +- TEN [dc]
152
+             +- TEN_PLUS [dc]
153
+             +- PROGRAM [dc]
154
+             +- CLEAR [dc]
155
+             +- RECALL [dc]
156
+             +- TIME_MODE [dc]
157
+             +- A_B_REPEAT [dc]
158
+             +- REPEAT [dc]
159
+             +- RANDOM [dc]
160
+             +- AUTO_CUE [dc]
161
+             +- TAPE_LENGTH [dc]
162
+             +- SIDE_A_B [dc]
163
+             +- TIME_FADE [dc]
164
+             +- PEAK_SEARCH [dc]
165
+             +- SEARCH_BACK [dc]
166
+             +- SEARCH_FOR [dc]
167
+             +- TRACK_NEXT [dc]
168
+             +- TRACK_PREV [dc]
169
+             +- STOP [dc]
170
+             +- PAUSE [dc]
171
+             +- PLAY [dc]
172
+    """
173
+    KEY_CD = "CD"
174
+    KEY_LINE1 = "LINE1"
175
+    KEY_LINE2 = "LINE2"
176
+    KEY_LINE3 = "LINE3"
177
+    KEY_PHONO = "PHONO"
178
+    KEY_MUTE = "MUTE"
179
+    KEY_POWER = "POWER"
180
+    KEY_VOLDOWN = "VOLDOWN"
181
+    KEY_VOLUP = "VOLUP"
182
+    #
183
+    TX_TOPIC = ''
184
+    TX_TYPE = base.TX_VALUE
185
+    #
186
+    RX_IGNORE_TOPICS = [KEY_CD, KEY_LINE1, KEY_LINE2, KEY_LINE3, KEY_PHONO, KEY_MUTE, KEY_POWER, KEY_VOLUP, KEY_VOLDOWN]
187
+
188
+    def __state_logging__(self, inst, key, data):
189
+        pass    # This is just a TX device using self.set_*
190
+
191
+    def set_cd(self, device=None, key=None, data=None):
192
+        self.logger.info("Changing amplifier source to CD")
193
+        self.send_command(self.KEY_CD, None)
194
+
195
+    def set_line1(self, device=None, key=None, data=None):
196
+        self.logger.info("Changing amplifier source to LINE1")
197
+        self.send_command(self.KEY_LINE1, None)
198
+
199
+    def set_line2(self, device=None, key=None, data=None):
200
+        self.logger.info("Changing amplifier source to LINE2")
201
+        self.send_command(self.KEY_LINE2, None)
202
+
203
+    def set_line3(self, device=None, key=None, data=None):
204
+        self.logger.info("Changing amplifier source to LINE3")
205
+        self.send_command(self.KEY_LINE3, None)
206
+
207
+    def set_phono(self, device=None, key=None, data=None):
208
+        self.logger.info("Changing amplifier source to PHONO")
209
+        self.send_command(self.KEY_PHONO, None)
210
+
211
+    def set_mute(self, device=None, key=None, data=None):
212
+        self.logger.info("Muting / Unmuting amplifier")
213
+        self.send_command(self.KEY_MUTE, None)
214
+
215
+    def set_power(self, device=None, key=None, data=None):
216
+        self.logger.info("Power on/off amplifier")
217
+        self.send_command(self.KEY_POWER, None)
218
+
219
+    def set_volume_up(self, data=False):
220
+        """data: [True, False]"""
221
+        self.logger.info("Increasing amplifier volume")
222
+        self.send_command(self.KEY_VOLUP, data)
223
+
224
+    def set_volume_down(self, data=False):
225
+        """data: [True, False]"""
226
+        self.logger.info("Decreasing amplifier volume")
227
+        self.send_command(self.KEY_VOLDOWN, data)
228
+
229
+    def default_inc(self, device=None, key=None, data=None):
230
+        self.set_volume_up(True)
231
+
232
+    def default_dec(self, device=None, key=None, data=None):
233
+        self.set_volume_down(True)
234
+
235
+    def default_stop(self, device=None, key=None, data=None):
236
+        self.set_volume_up(False)
237
+
238
+
239
+class audio_status(base):
240
+    """ Communication (MQTT)
241
+
242
+        audio_status
243
+            +- state [True, False]                  <- status
244
+            +- title [text]                         <- status
245
+    """
246
+    KEY_STATE = "state"
247
+    KEY_TITLE = "title"
248
+    #
249
+    TX_TYPE = base.TX_VALUE
250
+    #
251
+    RX_KEYS = [KEY_STATE, KEY_TITLE]
252
+
253
+    def __state_logging__(self, inst, key, data):
254
+        if key in [self.KEY_STATE, self.KEY_TITLE]:
255
+            self.logger.info("State change of '%s' to '%s'", key, repr(data))
256
+
257
+    def set_state(self, num, data):
258
+        """data: [True, False]"""
259
+        self.send_command(self.KEY_STATE + "/" + str(num), data)
260
+
261
+    def set_state_mcb(self, device, key, data):
262
+        self.set_state(data)

+ 171
- 0
devices/shelly.py 查看文件

@@ -0,0 +1,171 @@
1
+#!/usr/bin/env python
2
+# -*- coding: utf-8 -*-
3
+#
4
+from devices.base import base_output
5
+import logging
6
+import task
7
+
8
+
9
+class shelly(base_output):
10
+    """ Communication (MQTT)
11
+
12
+        shelly
13
+            +- relay
14
+            |      +- 0 ["on" / "off"]              <- status 
15
+            |      |  +- command ["on"/ "off"]      <- command
16
+            |      |  +- energy [numeric]           <- status
17
+            |      +- 1 ["on" / "off"]              <- status
18
+            |         +- command ["on"/ "off"]      <- command
19
+            |         +- energy [numeric]           <- status
20
+            +- input
21
+            |      +- 0 [0 / 1]                     <- status
22
+            |      +- 1 [0 / 1]                     <- status
23
+            +- input_event
24
+            |      +- 0                             <- status
25
+            |      +- 1                             <- status
26
+            +- logpush
27
+            |      +- 0 [0 / 1]                     <- status
28
+            |      +- 1 [0 / 1]                     <- status
29
+            +- temperature [numeric] °C             <- status
30
+            +- temperature_f [numeric] F            <- status
31
+            +- overtemperature [0 / 1]              <- status
32
+            +- id                                   <- status
33
+            +- model                                <- status
34
+            +- mac                                  <- status
35
+            +- ip                                   <- status
36
+            +- new_fw                               <- status
37
+            +- fw_ver                               <- status
38
+    """
39
+    KEY_OUTPUT_0 = "relay/0"
40
+    KEY_OUTPUT_1 = "relay/1"
41
+    KEY_INPUT_0 = "input/0"
42
+    KEY_INPUT_1 = "input/1"
43
+    KEY_LONGPUSH_0 = "longpush/0"
44
+    KEY_LONGPUSH_1 = "longpush/1"
45
+    KEY_TEMPERATURE = "temperature"
46
+    KEY_OVERTEMPERATURE = "overtemperature"
47
+    KEY_ID = "id"
48
+    KEY_MODEL = "model"
49
+    KEY_MAC = "mac"
50
+    KEY_IP = "ip"
51
+    KEY_NEW_FIRMWARE = "new_fw"
52
+    KEY_FIRMWARE_VERSION = "fw_ver"
53
+    #
54
+    TX_TOPIC = "command"
55
+    TX_TYPE = base_output.TX_VALUE
56
+    TX_FILTER_DATA_KEYS = [KEY_OUTPUT_0, KEY_OUTPUT_1]
57
+    #
58
+    RX_KEYS = [KEY_OUTPUT_0, KEY_OUTPUT_1, KEY_INPUT_0, KEY_INPUT_1, KEY_LONGPUSH_0, KEY_LONGPUSH_1, KEY_OVERTEMPERATURE, KEY_TEMPERATURE,
59
+               KEY_ID, KEY_MODEL, KEY_MAC, KEY_IP, KEY_NEW_FIRMWARE, KEY_FIRMWARE_VERSION]
60
+    RX_IGNORE_TOPICS = [KEY_OUTPUT_0 + '/' + "energy", KEY_OUTPUT_1 + '/' + "energy", 'input_event/0', 'input_event/1']
61
+    RX_IGNORE_KEYS = ['temperature_f']
62
+    RX_FILTER_DATA_KEYS = [KEY_INPUT_0, KEY_INPUT_1, KEY_LONGPUSH_0, KEY_LONGPUSH_1, KEY_OUTPUT_0, KEY_OUTPUT_1, KEY_OVERTEMPERATURE]
63
+
64
+    def __init__(self, mqtt_client, topic):
65
+        super().__init__(mqtt_client, topic)
66
+        #
67
+        self.output_key_delayed = None
68
+        self.delayed_flash_task = task.delayed(0.3, self.flash_task)
69
+        self.delayed_off_task = task.delayed(0.3, self.off_task)
70
+        #
71
+        self.all_off_requested = False
72
+
73
+    def __state_logging__(self, inst, key, data):
74
+        if key in [self.KEY_OUTPUT_0, self.KEY_OUTPUT_1]:
75
+            self.logger.info("State change of '%s' to '%s'", key, repr(data))
76
+        elif key in [self.KEY_INPUT_0, self.KEY_INPUT_1, self.KEY_LONGPUSH_0, self.KEY_LONGPUSH_1]:
77
+            self.logger.info("Input action '%s' with '%s'", key, repr(data))
78
+
79
+    def flash_task(self, *args):
80
+        if self.flash_active:
81
+            self.send_command(self.output_key_delayed, not self.get(self.output_key_delayed))
82
+            self.output_key_delayed = None
83
+            if self.all_off_requested:
84
+                self.delayed_off_task.run()
85
+
86
+    def off_task(self, *args):
87
+        self.all_off()
88
+
89
+    @property
90
+    def flash_active(self):
91
+        return self.output_key_delayed is not None
92
+
93
+    #
94
+    # RX
95
+    #
96
+    @property
97
+    def output_0(self):
98
+        """rv: [True, False]"""
99
+        return self.get(self.KEY_OUTPUT_0)
100
+
101
+    @property
102
+    def output_1(self):
103
+        """rv: [True, False]"""
104
+        return self.get(self.KEY_OUTPUT_1)
105
+
106
+    @property
107
+    def input_0(self):
108
+        """rv: [True, False]"""
109
+        return self.get(self.KEY_INPUT_0)
110
+
111
+    @property
112
+    def input_1(self):
113
+        """rv: [True, False]"""
114
+        return self.get(self.KEY_INPUT_1)
115
+
116
+    @property
117
+    def longpush_0(self):
118
+        """rv: [True, False]"""
119
+        return self.get(self.KEY_LONGPUSH_0)
120
+
121
+    @property
122
+    def longpush_1(self):
123
+        """rv: [True, False]"""
124
+        return self.get(self.KEY_LONGPUSH_1)
125
+
126
+    @property
127
+    def temperature(self):
128
+        """rv: numeric value"""
129
+        return self.get(self.KEY_TEMPERATURE)
130
+
131
+    #
132
+    # TX
133
+    #
134
+    def set_output_0(self, state):
135
+        """state: [True, False]"""
136
+        self.send_command(self.KEY_OUTPUT_0, state)
137
+
138
+    def set_output_0_mcb(self, device, key, data):
139
+        self.set_output_0(data)
140
+
141
+    def toggle_output_0_mcb(self, device, key, data):
142
+        self.set_output_0(not self.output_0)
143
+
144
+    def set_output_1(self, state):
145
+        """state: [True, False]"""
146
+        self.send_command(self.KEY_OUTPUT_1, state)
147
+
148
+    def set_output_1_mcb(self, device, key, data):
149
+        self.set_output_1(data)
150
+
151
+    def toggle_output_1_mcb(self, device, key, data):
152
+        self.set_output_1(not self.output_1)
153
+
154
+    def flash_0_mcb(self, device, key, data):
155
+        self.output_key_delayed = self.KEY_OUTPUT_0
156
+        self.toggle_output_0_mcb(device, key, data)
157
+        self.delayed_flash_task.run()
158
+
159
+    def flash_1_mcb(self, device, key, data):
160
+        self.output_key_delayed = self.KEY_OUTPUT_1
161
+        self.toggle_output_1_mcb(device, key, data)
162
+        self.delayed_flash_task.run()
163
+
164
+    def __all_off__(self):
165
+        if self.flash_active:
166
+            self.all_off_requested = True
167
+        else:
168
+            if self.output_0:
169
+                self.set_output_0(False)
170
+            if self.output_1:
171
+                self.set_output_1(False)

+ 107
- 0
devices/silvercrest.py 查看文件

@@ -0,0 +1,107 @@
1
+#!/usr/bin/env python
2
+# -*- coding: utf-8 -*-
3
+#
4
+from devices.base import base, base_output
5
+import logging
6
+
7
+
8
+class silvercrest_powerplug(base_output):
9
+    """ Communication (MQTT)
10
+
11
+        silvercrest_powerplug {
12
+                            |      "state": ["ON" / "OFF"]
13
+                            |      "linkquality": [0...255] lqi
14
+                            | }
15
+                            +- get {
16
+                            |           "state": ""
17
+                            |      }
18
+                            +- set {
19
+                                        "state": ["ON" / "OFF"]
20
+                                   }
21
+    """
22
+    KEY_LINKQUALITY = "linkquality"
23
+    KEY_OUTPUT_0 = "state"
24
+    #
25
+    TX_TYPE = base.TX_DICT
26
+    TX_FILTER_DATA_KEYS = [KEY_OUTPUT_0]
27
+    #
28
+    RX_KEYS = [KEY_LINKQUALITY, KEY_OUTPUT_0]
29
+    RX_FILTER_DATA_KEYS = [KEY_OUTPUT_0]
30
+
31
+    def __state_logging__(self, inst, key, data):
32
+        if key in [self.KEY_OUTPUT_0]:
33
+            self.logger.info("State change of '%s' to '%s'", key, repr(data))
34
+
35
+    #
36
+    # RX
37
+    #
38
+    @property
39
+    def output_0(self):
40
+        """rv: [True, False]"""
41
+        return self.get(self.KEY_OUTPUT_0)
42
+
43
+    @property
44
+    def linkquality(self):
45
+        """rv: numeric value"""
46
+        return self.get(self.KEY_LINKQUALITY)
47
+
48
+    #
49
+    # TX
50
+    #
51
+    def set_output_0(self, state):
52
+        """state: [True, False]"""
53
+        self.send_command(self.KEY_OUTPUT_0, state)
54
+
55
+    def set_output_0_mcb(self, device, key, data):
56
+        self.set_output_0(data)
57
+
58
+    def toggle_output_0_mcb(self, device, key, data):
59
+        self.set_output_0(not self.output_0)
60
+
61
+    def __all_off__(self):
62
+        if self.output_0:
63
+            self.set_output_0(False)
64
+
65
+
66
+class silvercrest_motion_sensor(base):
67
+    """ Communication (MQTT)
68
+
69
+        silvercrest_motion_sensor {
70
+                                      battery: [0...100] %
71
+                                      battery_low: [True, False]
72
+                                      linkquality: [0...255] lqi
73
+                                      occupancy: [True, False]
74
+                                      tamper: [True, False]
75
+                                      voltage: [0...] mV
76
+                                  }
77
+    """
78
+    KEY_BATTERY = "battery"
79
+    KEY_BATTERY_LOW = "battery_low"
80
+    KEY_LINKQUALITY = "linkquality"
81
+    KEY_OCCUPANCY = "occupancy"
82
+    KEY_UNMOUNTED = "tamper"
83
+    KEY_VOLTAGE = "voltage"
84
+    #
85
+    TX_TYPE = base.TX_DICT
86
+    #
87
+    RX_KEYS = [KEY_BATTERY, KEY_BATTERY_LOW, KEY_LINKQUALITY, KEY_OCCUPANCY, KEY_UNMOUNTED, KEY_VOLTAGE]
88
+
89
+    def __init__(self, mqtt_client, topic):
90
+        super().__init__(mqtt_client, topic)
91
+
92
+    def __state_logging__(self, inst, key, data):
93
+        if key in [self.KEY_OCCUPANCY, self.KEY_UNMOUNTED]:
94
+            self.logger.info("State change of '%s' to '%s'", key, repr(data))
95
+
96
+    #
97
+    # RX
98
+    #
99
+    @property
100
+    def linkquality(self):
101
+        """rv: numeric value"""
102
+        return self.get(self.KEY_LINKQUALITY)
103
+
104
+    @property
105
+    def battery(self):
106
+        """rv: numeric value"""
107
+        return self.get(self.KEY_BATTERY)

+ 192
- 0
devices/tradfri.py 查看文件

@@ -0,0 +1,192 @@
1
+#!/usr/bin/env python
2
+# -*- coding: utf-8 -*-
3
+#
4
+from devices.base import base, base_output
5
+import logging
6
+
7
+
8
+class tradfri_light(base_output):
9
+    """ Communication (MQTT)
10
+
11
+        tradfri_light {
12
+                    |      "state": ["ON" / "OFF" / "TOGGLE"]
13
+                    |      "linkquality": [0...255] lqi
14
+                    |      "brightness": [0...254]
15
+                    |      "color_mode": ["color_temp"]
16
+                    |      "color_temp": ["coolest", "cool", "neutral", "warm", "warmest", 250...454]
17
+                    |      "color_temp_startup": ["coolest", "cool", "neutral", "warm", "warmest", "previous", 250...454]
18
+                    |      "update": []
19
+                    | }
20
+                    +- get {
21
+                    |           "state": ""
22
+                    |      }
23
+                    +- set {
24
+                                "state": ["ON" / "OFF"]
25
+                                "brightness": [0...256]
26
+                                "color_temp": [250...454]
27
+                                "transition": [0...] seconds
28
+                                "brightness_move": [-X...0...X] X/s
29
+                                "brightness_step": [-X...0...X]
30
+                                "color_temp_move": [-X...0...X] X/s
31
+                                "color_temp_step": [-X...0...X]
32
+                            }
33
+    """
34
+    KEY_LINKQUALITY = "linkquality"
35
+    KEY_OUTPUT_0 = "state"
36
+    KEY_BRIGHTNESS = "brightness"
37
+    KEY_COLOR_TEMP = "color_temp"
38
+    KEY_BRIGHTNESS_FADE = "brightness_move"
39
+    #
40
+    TX_TYPE = base.TX_DICT
41
+    TX_FILTER_DATA_KEYS = [KEY_OUTPUT_0, KEY_BRIGHTNESS, KEY_COLOR_TEMP, KEY_BRIGHTNESS_FADE]
42
+    #
43
+    RX_KEYS = [KEY_LINKQUALITY, KEY_OUTPUT_0, KEY_BRIGHTNESS, KEY_COLOR_TEMP]
44
+    RX_IGNORE_KEYS = ['update', 'color_mode', 'color_temp_startup']
45
+    RX_FILTER_DATA_KEYS = [KEY_OUTPUT_0, KEY_BRIGHTNESS, KEY_COLOR_TEMP]
46
+
47
+    def __state_logging__(self, inst, key, data):
48
+        if key in [self.KEY_OUTPUT_0, self.KEY_BRIGHTNESS, self.KEY_COLOR_TEMP, self.KEY_BRIGHTNESS_FADE]:
49
+            self.logger.info("State change of '%s' to '%s'", key, repr(data))
50
+
51
+    def __device_to_instance_filter__(self, key, data):
52
+        if key == self.KEY_BRIGHTNESS:
53
+            return int(round((data - 1) * 100 / 253, 0))
54
+        elif key == self.KEY_COLOR_TEMP:
55
+            return int(round((data - 250) * 10 / 204, 0))
56
+        return super().__device_to_instance_filter__(key, data)
57
+
58
+    def __instance_to_device_filter__(self, key, data):
59
+        if key == self.KEY_BRIGHTNESS:
60
+            return int(round(data * 253 / 100 + 1, 0))
61
+        elif key == self.KEY_COLOR_TEMP:
62
+            return int(round(data * 204 / 10 + 250, 0))
63
+        return super().__instance_to_device_filter__(key, data)
64
+
65
+    #
66
+    # RX
67
+    #
68
+    @property
69
+    def output_0(self):
70
+        """rv: [True, False]"""
71
+        return self.get(self.KEY_OUTPUT_0, False)
72
+
73
+    @property
74
+    def linkquality(self):
75
+        """rv: numeric value"""
76
+        return self.get(self.KEY_LINKQUALITY, 0)
77
+
78
+    @property
79
+    def brightness(self):
80
+        """rv: numeric value [0%, ..., 100%]"""
81
+        return self.get(self.KEY_BRIGHTNESS, 0)
82
+
83
+    @property
84
+    def color_temp(self):
85
+        """rv: numeric value [0, ..., 10]"""
86
+        return self.get(self.KEY_COLOR_TEMP, 0)
87
+
88
+    #
89
+    # TX
90
+    #
91
+    def request_data(self, device=None, key=None, data=None):
92
+        self.mqtt_client.send(self.topic + "/get", '{"%s": ""}' % self.KEY_OUTPUT_0)
93
+
94
+    def set_output_0(self, state):
95
+        """state: [True, False]"""
96
+        self.send_command(self.KEY_OUTPUT_0, state)
97
+
98
+    def set_output_0_mcb(self, device, key, data):
99
+        self.set_output_0(data)
100
+
101
+    def toggle_output_0_mcb(self, device, key, data):
102
+        self.set_output_0(not self.output_0)
103
+
104
+    def set_brightness(self, brightness):
105
+        """brightness: [0, ..., 100]"""
106
+        self.send_command(self.KEY_BRIGHTNESS, brightness)
107
+
108
+    def set_brightness_mcb(self, device, key, data):
109
+        self.set_brightness(data)
110
+
111
+    def default_inc(self, speed=40):
112
+        self.send_command(self.KEY_BRIGHTNESS_FADE, speed)
113
+
114
+    def default_dec(self, speed=-40):
115
+        self.default_inc(speed)
116
+
117
+    def default_stop(self):
118
+        self.default_inc(0)
119
+
120
+    def set_color_temp(self, color_temp):
121
+        """color_temp: [0, ..., 10]"""
122
+        self.send_command(self.KEY_COLOR_TEMP, color_temp)
123
+
124
+    def set_color_temp_mcb(self, device, key, data):
125
+        self.set_color_temp(data)
126
+
127
+    def __all_off__(self):
128
+        if self.output_0:
129
+            self.set_output_0(False)
130
+
131
+
132
+class tradfri_button(base):
133
+    """ Communication (MQTT)
134
+
135
+        tradfri_button {
136
+                            "action": [
137
+                                           "arrow_left_click",
138
+                                           "arrow_left_hold",
139
+                                           "arrow_left_release",
140
+                                           "arrow_right_click",
141
+                                           "arrow_right_hold",
142
+                                           "arrow_right_release",
143
+                                           "brightness_down_click",
144
+                                           "brightness_down_hold",
145
+                                           "brightness_down_release",
146
+                                           "brightness_up_click",
147
+                                           "brightness_up_hold",
148
+                                           "brightness_up_release",
149
+                                           "toggle"
150
+                                      ]
151
+                            "action_duration": [0...] s
152
+                            "battery": [0...100] %
153
+                            "linkquality": [0...255] lqi
154
+                            "update": []
155
+                       }
156
+    """
157
+    ACTION_TOGGLE = "toggle"
158
+    ACTION_BRIGHTNESS_UP = "brightness_up_click"
159
+    ACTION_BRIGHTNESS_DOWN = "brightness_down_click"
160
+    ACTION_RIGHT = "arrow_right_click"
161
+    ACTION_LEFT = "arrow_left_click"
162
+    ACTION_BRIGHTNESS_UP_LONG = "brightness_up_hold"
163
+    ACTION_BRIGHTNESS_UP_RELEASE = "brightness_up_release"
164
+    ACTION_BRIGHTNESS_DOWN_LONG = "brightness_down_hold"
165
+    ACTION_BRIGHTNESS_DOWN_RELEASE = "brightness_down_release"
166
+    ACTION_RIGHT_LONG = "arrow_right_hold"
167
+    ACTION_RIGHT_RELEASE = "arrow_right_release"
168
+    ACTION_LEFT_LONG = "arrow_left_hold"
169
+    ACTION_LEFT_RELEASE = "arrow_left_release"
170
+    #
171
+    KEY_LINKQUALITY = "linkquality"
172
+    KEY_BATTERY = "battery"
173
+    KEY_ACTION = "action"
174
+    KEY_ACTION_DURATION = "action_duration"
175
+    #
176
+    RX_KEYS = [KEY_LINKQUALITY, KEY_BATTERY, KEY_ACTION]
177
+    RX_IGNORE_KEYS = ['update', KEY_ACTION_DURATION]
178
+
179
+    def __init__(self, mqtt_client, topic):
180
+        super().__init__(mqtt_client, topic)
181
+
182
+    def __state_logging__(self, inst, key, data):
183
+        if key in [self.KEY_ACTION]:
184
+            self.logger.info("Input '%s' with '%s'", key, repr(data))
185
+
186
+    #
187
+    # RX
188
+    #
189
+    @property
190
+    def action(self):
191
+        """rv: action_txt"""
192
+        return self.get(self.KEY_ACTION)

+ 1
- 0
geo

@@ -0,0 +1 @@
1
+Subproject commit 11166bb27ad2335f7812fcb88c788397f5106751

+ 1
- 0
mqtt

@@ -0,0 +1 @@
1
+Subproject commit 14e56ccdbf6594f699b4afcfb4acafe9b899e914

+ 1
- 0
report

@@ -0,0 +1 @@
1
+Subproject commit 7003c13ef8c7e7c3a55a545cbbad4039cc024a9f

+ 129
- 0
smart_enlife.py 查看文件

@@ -0,0 +1,129 @@
1
+import config
2
+import devices
3
+import geo
4
+import logging
5
+import geo.sun
6
+import mqtt
7
+import random
8
+import report
9
+import state_machine
10
+import time
11
+
12
+logger = logging.getLogger(config.APP_NAME)
13
+
14
+
15
+class day_states(state_machine.state_machine):
16
+    LOG_PREFIX = 'Day states:'
17
+
18
+    STATE_NIGHT = 'state_night'
19
+    STATE_MORNING = 'state_morning'
20
+    STATE_DAY = 'state_day'
21
+    STATE_EVENING = 'state_evening'
22
+
23
+    CONDITION_NIGHT_END = 'condition_night_end'
24
+    CONDITION_MORNING_END = 'condition_morning_end'
25
+    CONDITION_DAY_END = 'condition_day_end'
26
+    CONDITION_EVENING_END = 'condition_evening_end'
27
+
28
+    TRANSITIONS = {
29
+        STATE_NIGHT: (
30
+            (CONDITION_NIGHT_END, 1, STATE_MORNING),
31
+        ),
32
+        STATE_MORNING: (
33
+            (CONDITION_MORNING_END, 1, STATE_DAY),
34
+        ),
35
+        STATE_DAY: (
36
+            (CONDITION_DAY_END, 1, STATE_EVENING),
37
+        ),
38
+        STATE_EVENING: (
39
+            (CONDITION_EVENING_END, 1, STATE_NIGHT),
40
+        ),
41
+    }
42
+
43
+    def condition_night_end(self):
44
+        ltime = time.localtime(time.time())
45
+        return ltime.tm_hour >= 5 and ltime.tm_min >= 20 and not self.condition_morning_end()   # 2nd condition to ensure day change
46
+
47
+    def condition_morning_end(self):
48
+        ltime = time.mktime(time.localtime(time.time()))
49
+        sunrise = time.mktime(geo.sun.sunrise(config.GEO_POSITION)) + 30 * 60
50
+        return ltime > sunrise
51
+
52
+    def condition_day_end(self):
53
+        ltime = time.mktime(time.localtime(time.time()))
54
+        sunset = time.mktime(geo.sun.sunset(config.GEO_POSITION)) - 30 * 60
55
+        return ltime > sunset
56
+
57
+    def condition_evening_end(self):
58
+        ltime = time.localtime(time.time())
59
+        return ltime.tm_hour >= 20 and ltime.tm_min >= 30
60
+
61
+
62
+def get_sorted_sw_offs(num):
63
+    SWITCH_DURATION = 30 * 60
64
+    rv = []
65
+    for i in range(0, num):
66
+        rv.append(random.randint(0, SWITCH_DURATION))
67
+    rv.sort()
68
+    return rv
69
+
70
+
71
+def switch_x(state: bool, sm: day_states, mydevs: list):
72
+    tm = time.time()
73
+    random.shuffle(mydevs)
74
+    offsets = get_sorted_sw_offs(len(mydevs))
75
+    logger.info("State changed to %s with offsets = %s", repr(sm.this_state()), repr(offsets))
76
+    allowed_states = [sm.STATE_EVENING, sm.STATE_MORNING] if state else [sm.STATE_DAY, sm.STATE_NIGHT]
77
+    while (len(mydevs) > 0 and sm.this_state() in allowed_states):
78
+        sm.work()
79
+        dt = time.time() - tm
80
+        if dt > offsets[0]:
81
+            offsets.pop(0)
82
+            d: devices.tradfri_sw_br_ct = mydevs.pop()
83
+            logger.info("Swiching %s to state %s", d.topic, repr(state))
84
+            d.set_output_0(state)
85
+        time.sleep(0.25)
86
+
87
+
88
+def switch_on(sm: day_states, devs: list):
89
+    switch_x(True, sm, devs[:])
90
+
91
+
92
+def switch_off(sm: day_states, devs: list):
93
+    switch_x(False, sm, devs[:])
94
+
95
+
96
+if __name__ == "__main__":
97
+    #
98
+    # Logging
99
+    #
100
+    if config.DEBUG:
101
+        report.appLoggingConfigure(None, 'stdout', ((config.APP_NAME, logging.DEBUG), ),
102
+                                   target_level=logging.WARNING, fmt=report.SHORT_FMT, host='localhost', port=19996)
103
+    else:
104
+        report.stdoutLoggingConfigure(((config.APP_NAME, config.LOGLEVEL), ), report.SHORT_FMT)
105
+
106
+    #
107
+    # MQTT Client
108
+    #
109
+    mc = mqtt.mqtt_client(host=config.MQTT_SERVER, port=config.MQTT_PORT, username=config.MQTT_USER,
110
+                          password=config.MQTT_PASSWORD, name=config.APP_NAME)
111
+
112
+    #
113
+    # Smarthome physical Devices
114
+    devs = []
115
+    devs.append(devices.tradfri_sw_br_ct(mc, "zigbee_gfe/gfe/room1/window_light"))
116
+    devs.append(devices.tradfri_sw_br_ct(mc, "zigbee_gfe/gfe/room2/window_light"))
117
+    devs.append(devices.tradfri_sw_br_ct(mc, "zigbee_gfe/gfe/room3/window_light"))
118
+
119
+    #
120
+    # Functionality
121
+    #
122
+    sm = day_states(day_states.STATE_DAY, logging.DEBUG)
123
+    sm.register_state_change_callback(sm.STATE_NIGHT, None, switch_off, sm, devs)
124
+    sm.register_state_change_callback(sm.STATE_MORNING, None, switch_on, sm, devs)
125
+    sm.register_state_change_callback(sm.STATE_DAY, None, switch_off, sm, devs)
126
+    sm.register_state_change_callback(sm.STATE_EVENING, None, switch_on, sm, devs)
127
+    while True:
128
+        sm.work()
129
+        time.sleep(0.25)

+ 4
- 0
smart_enlife.sh 查看文件

@@ -0,0 +1,4 @@
1
+#!/bin/sh
2
+#
3
+BASEPATH=`dirname $0`
4
+$BASEPATH/venv/bin/python $BASEPATH/smart_enlife.py

+ 1
- 0
state_machine

@@ -0,0 +1 @@
1
+Subproject commit aef99580e1cdc05e6ca80efe05461ab861d6ef39

+ 1
- 0
task

@@ -0,0 +1 @@
1
+Subproject commit af35e83d1f07fd4cb9070bdb77cf1f3bdda3a463

Loading…
取消
儲存