qbusmqttapi 0.2.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Qbus
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.1
2
+ Name: qbusmqttapi
3
+ Version: 0.2.7
4
+ Summary: MQTT API for Qbus Home Automation.
5
+ Home-page: https://github.com/Qbus-iot/qbusmqttapi
6
+ Author: Koen Schockaert
7
+ Author-email: ks@qbus.be
8
+ License: MIT License 2024
9
+ Project-URL: Source Code, https://github.com/Qbus-iot/qbusmqttapi
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+
17
+ # qbusmqttapi
18
+ Python MQTT API for Qbus Home Automation
@@ -0,0 +1,2 @@
1
+ # qbusmqttapi
2
+ Python MQTT API for Qbus Home Automation
@@ -0,0 +1,26 @@
1
+ [tool.ruff]
2
+ exclude = [".git", ".vscode", ".pytest_cache", ".mypy_cache", ".env"]
3
+ line-length = 125
4
+
5
+ [tool.ruff.lint]
6
+ ignore = ["B008", "ISC001", "E501", "W191"]
7
+ select = [
8
+ "B",
9
+ "E",
10
+ "F",
11
+ "W",
12
+ "I",
13
+ "N",
14
+ "C4",
15
+ "EXE",
16
+ "ISC",
17
+ "ICN",
18
+ "PIE",
19
+ "PT",
20
+ "RET",
21
+ "SIM",
22
+ "ERA",
23
+ "PLC",
24
+ "RUF",
25
+ "ARG",
26
+ ]
@@ -0,0 +1,7 @@
1
+ [metadata]
2
+ license_files = LICENSE
3
+
4
+ [egg_info]
5
+ tag_build =
6
+ tag_date = 0
7
+
@@ -0,0 +1,27 @@
1
+ from pathlib import Path
2
+
3
+ import setuptools
4
+
5
+ VERSION = "0.2.7" # PEP-440
6
+
7
+ setuptools.setup(
8
+ name="qbusmqttapi",
9
+ version=VERSION,
10
+ description="MQTT API for Qbus Home Automation.",
11
+ url="https://github.com/Qbus-iot/qbusmqttapi",
12
+ project_urls={
13
+ "Source Code": "https://github.com/Qbus-iot/qbusmqttapi",
14
+ },
15
+ author="Koen Schockaert",
16
+ author_email="ks@qbus.be",
17
+ license="MIT License 2024",
18
+ classifiers=[
19
+ "Development Status :: 3 - Alpha",
20
+ "Environment :: Console",
21
+ "Programming Language :: Python :: 3.10",
22
+ ],
23
+ python_requires=">=3.8",
24
+ # Requirements
25
+ long_description=Path("README.md").read_text(),
26
+ long_description_content_type="text/markdown",
27
+ )
@@ -0,0 +1 @@
1
+ """QBUS MQTT API."""
@@ -0,0 +1,14 @@
1
+ """Qbus MQTT API constants."""
2
+
3
+ TOPIC_PREFIX = "cloudapp/QBUSMQTTGW"
4
+
5
+ KEY_OUTPUT_ACTION = "action"
6
+ KEY_OUTPUT_ACTIONS = "actions"
7
+ KEY_OUTPUT_ID = "id"
8
+ KEY_OUTPUT_NAME = "name"
9
+ KEY_OUTPUT_PROPERTIES = "properties"
10
+ KEY_OUTPUT_REF_ID = "refId"
11
+ KEY_OUTPUT_TYPE = "type"
12
+
13
+ KEY_PROPERTIES_AUTHKEY = "authKey"
14
+ KEY_PROPERTIES_VALUE = "value"
@@ -0,0 +1,160 @@
1
+ """Qbis discovery models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .const import (
6
+ KEY_OUTPUT_ACTIONS,
7
+ KEY_OUTPUT_ID,
8
+ KEY_OUTPUT_NAME,
9
+ KEY_OUTPUT_PROPERTIES,
10
+ KEY_OUTPUT_REF_ID,
11
+ KEY_OUTPUT_TYPE,
12
+ )
13
+
14
+
15
+ KEY_DEVICES = "devices"
16
+
17
+ KEY_DEVICE_FUNCTIONBLOCKS = "functionBlocks"
18
+ KEY_DEVICE_ID = "id"
19
+ KEY_DEVICE_IP = "ip"
20
+ KEY_DEVICE_MAC = "mac"
21
+ KEY_DEVICE_NAME = "name"
22
+ KEY_DEVICE_SERIAL_NR = "serialNr"
23
+ KEY_DEVICE_TYPE = "type"
24
+ KEY_DEVICE_VERSION = "version"
25
+
26
+
27
+ class QbusMqttOutput:
28
+ """MQTT representation of a Qbus output."""
29
+
30
+ def __init__(self, data: dict, device: QbusMqttDevice) -> None:
31
+ """Initialize based on a json loaded dictionary."""
32
+ self._data = data
33
+ self._device = device
34
+
35
+ @property
36
+ def id(self) -> str:
37
+ """Return the id."""
38
+ return self._data.get(KEY_OUTPUT_ID) or ""
39
+
40
+ @property
41
+ def type(self) -> str:
42
+ """Return the type."""
43
+ return self._data.get(KEY_OUTPUT_TYPE) or ""
44
+
45
+ @property
46
+ def name(self) -> str:
47
+ """Return the name."""
48
+ return self._data.get(KEY_OUTPUT_NAME) or ""
49
+
50
+ @property
51
+ def ref_id(self) -> str:
52
+ """Return the ref id."""
53
+ return self._data.get(KEY_OUTPUT_REF_ID) or ""
54
+
55
+ @property
56
+ def properties(self) -> dict:
57
+ """Return the properties."""
58
+ return self._data.get(KEY_OUTPUT_PROPERTIES) or {}
59
+
60
+ @property
61
+ def actions(self) -> dict:
62
+ """Return the actions."""
63
+ return self._data.get(KEY_OUTPUT_ACTIONS) or {}
64
+
65
+ @property
66
+ def device(self) -> QbusMqttDevice:
67
+ """Return the device."""
68
+ return self._device
69
+
70
+
71
+ class QbusMqttDevice:
72
+ """MQTT representation of a Qbus device."""
73
+
74
+ def __init__(self, data: dict) -> None:
75
+ """Initialize based on a json loaded dictionary."""
76
+ self._data = data
77
+
78
+ @property
79
+ def id(self) -> str:
80
+ """Return the id."""
81
+ return self._data.get(KEY_DEVICE_ID) or ""
82
+
83
+ @property
84
+ def ip(self) -> str:
85
+ """Return the ip address."""
86
+ return self._data.get(KEY_DEVICE_IP) or ""
87
+
88
+ @property
89
+ def mac(self) -> str:
90
+ """Return the ip address."""
91
+ return self._data.get(KEY_DEVICE_MAC) or ""
92
+
93
+ @property
94
+ def name(self) -> str:
95
+ """Return the ip address."""
96
+ return self._data.get(KEY_DEVICE_NAME) or ""
97
+
98
+ @property
99
+ def serial_number(self) -> str:
100
+ """Return the serial number."""
101
+ return self._data.get(KEY_DEVICE_SERIAL_NR) or ""
102
+
103
+ @property
104
+ def type(self) -> str:
105
+ """Return the mac address."""
106
+ return self._data.get(KEY_DEVICE_TYPE) or ""
107
+
108
+ @property
109
+ def version(self) -> str:
110
+ """Return the version."""
111
+ return self._data.get(KEY_DEVICE_VERSION) or ""
112
+
113
+ @property
114
+ def outputs(self) -> list[QbusMqttOutput]:
115
+ """Return the outputs."""
116
+
117
+ outputs: list[QbusMqttOutput] = []
118
+
119
+ if self._data.get(KEY_DEVICE_FUNCTIONBLOCKS):
120
+ outputs = [
121
+ QbusMqttOutput(x, self) for x in self._data[KEY_DEVICE_FUNCTIONBLOCKS]
122
+ ]
123
+
124
+ return outputs
125
+
126
+
127
+ class QbusDiscovery:
128
+ """MQTT representation of a Qbus config."""
129
+
130
+ def __init__(self, data: dict) -> None:
131
+ """Initialize based on a json loaded dictionary."""
132
+ if KEY_DEVICES in data:
133
+ self._devices = [QbusMqttDevice(x) for x in data[KEY_DEVICES]]
134
+
135
+ self._name = data["app"]
136
+
137
+ def get_device_by_id(self, id: str) -> QbusMqttDevice | None:
138
+ """Get the device by id."""
139
+ return next((x for x in self.devices if x.id.casefold() == id.casefold()), None)
140
+
141
+ def get_device_by_serial(self, serial: str) -> QbusMqttDevice | None:
142
+ """Get the device by serial."""
143
+ return next(
144
+ (
145
+ x
146
+ for x in self.devices
147
+ if x.serial_number.casefold() == serial.casefold()
148
+ ),
149
+ None,
150
+ )
151
+
152
+ @property
153
+ def devices(self) -> list[QbusMqttDevice]:
154
+ """Return the devices."""
155
+ return self._devices
156
+
157
+ @property
158
+ def name(self):
159
+ """Return device name."""
160
+ return self._name
@@ -0,0 +1,172 @@
1
+ """Qbus MQTT factory."""
2
+
3
+ from dataclasses import dataclass
4
+ import json
5
+ import logging
6
+ from typing import Any, TypeVar
7
+
8
+ from .const import (
9
+ KEY_PROPERTIES_AUTHKEY,
10
+ TOPIC_PREFIX,
11
+ )
12
+ from .discovery import QbusDiscovery, QbusMqttDevice
13
+ from .state import (
14
+ QbusMqttControllerState,
15
+ QbusMqttState,
16
+ StateAction,
17
+ StateType,
18
+ )
19
+
20
+ _LOGGER = logging.getLogger(__name__)
21
+
22
+
23
+ @dataclass
24
+ class QbusMqttRequestMessage:
25
+ """Qbus MQTT request data class."""
26
+
27
+ topic: str
28
+ payload: str | bytes
29
+
30
+
31
+ class QbusMqttMessageFactory:
32
+ """Factory methods for Qbus MQTT messages."""
33
+
34
+ T = TypeVar("T", bound="QbusMqttState")
35
+
36
+ def __init__(self) -> None:
37
+ self._topic_factory = QbusMqttTopicFactory()
38
+
39
+ def parse_discovery(self, payload: str | bytes) -> QbusDiscovery | None:
40
+ """Parse an MQTT message and return an instance
41
+ of QbusDiscovery if successful, otherwise None."""
42
+
43
+ discovery: QbusDiscovery | None = self.deserialize(QbusDiscovery, payload)
44
+
45
+ # Discovery data must include the Qbus device type and name.
46
+ if discovery is not None and len(discovery.devices) == 0:
47
+ _LOGGER.error("Incomplete discovery payload: %s", payload)
48
+ return None
49
+
50
+ return discovery
51
+
52
+ def parse_controller_state(
53
+ self, payload: str | bytes
54
+ ) -> QbusMqttControllerState | None:
55
+ """Parse an MQTT message and return an instance
56
+ of QbusMqttControllerState if successful, otherwise None."""
57
+
58
+ return self.deserialize(QbusMqttControllerState, payload)
59
+
60
+ def parse_output_state(self, cls: type[T], payload: str | bytes) -> T | None:
61
+ """Parse an MQTT message and return an instance
62
+ of T if successful, otherwise None."""
63
+
64
+ return self.deserialize(cls, payload)
65
+
66
+ def create_device_activate_request(
67
+ self, device: QbusMqttDevice, prefix: str = TOPIC_PREFIX
68
+ ) -> QbusMqttRequestMessage:
69
+ """Create a message to request device activation."""
70
+ state = QbusMqttState(
71
+ id=device.id, type=StateType.ACTION, action=StateAction.ACTIVATE
72
+ )
73
+ state.write_property(KEY_PROPERTIES_AUTHKEY, "ubielite")
74
+
75
+ return QbusMqttRequestMessage(
76
+ self._topic_factory.get_device_command_topic(device.id, prefix),
77
+ self.serialize(state),
78
+ )
79
+
80
+ def create_device_state_request(
81
+ self, device: QbusMqttDevice, prefix: str = TOPIC_PREFIX
82
+ ) -> QbusMqttRequestMessage:
83
+ """Create a message to request a device state."""
84
+ return QbusMqttRequestMessage(
85
+ self._topic_factory.get_get_state_topic(prefix), json.dumps([device.id])
86
+ )
87
+
88
+ def create_state_request(
89
+ self, entity_ids: list[str], prefix: str = TOPIC_PREFIX
90
+ ) -> QbusMqttRequestMessage:
91
+ """Create a message to request entity states."""
92
+ return QbusMqttRequestMessage(
93
+ self._topic_factory.get_get_state_topic(prefix), json.dumps(entity_ids)
94
+ )
95
+
96
+ def create_set_output_state_request(
97
+ self, device: QbusMqttDevice, state: QbusMqttState, prefix: str = TOPIC_PREFIX
98
+ ) -> QbusMqttRequestMessage:
99
+ """Create a message to update the output state."""
100
+ return QbusMqttRequestMessage(
101
+ self._topic_factory.get_output_command_topic(device.id, state.id, prefix),
102
+ self.serialize(state),
103
+ )
104
+
105
+ def serialize(self, obj: Any) -> str:
106
+ """Convert an object to json payload."""
107
+ return json.dumps(obj, cls=IgnoreNoneJsonEncoder)
108
+
109
+ def deserialize(self, state_cls: type[Any], payload: str | bytes) -> Any | None:
110
+ """Parse an MQTT message and return the requested type if successful, otherwise None."""
111
+
112
+ if payload is None:
113
+ _LOGGER.warning("Empty state payload for %s", state_cls.__name__)
114
+ return None
115
+
116
+ try:
117
+ data = json.loads(payload)
118
+ except ValueError:
119
+ _LOGGER.error(
120
+ "Invalid state payload for %s: %s", state_cls.__name__, payload
121
+ )
122
+ return None
123
+
124
+ return state_cls(data)
125
+
126
+
127
+ class QbusMqttTopicFactory:
128
+ """Factory methods for topics of the Qbus MQTT API."""
129
+
130
+ def get_get_config_topic(self, prefix: str = TOPIC_PREFIX) -> str:
131
+ """Return the getConfig topic."""
132
+ return f"{prefix}/getConfig"
133
+
134
+ def get_config_topic(self, prefix: str = TOPIC_PREFIX) -> str:
135
+ """Return the config topic."""
136
+ return f"{prefix}/config"
137
+
138
+ def get_get_state_topic(self, prefix: str = TOPIC_PREFIX) -> str:
139
+ """Return the getState topic."""
140
+ return f"{prefix}/getState"
141
+
142
+ def get_device_state_topic(self, device_id: str, prefix: str = TOPIC_PREFIX) -> str:
143
+ """Return the state topic."""
144
+ return f"{prefix}/{device_id}/state"
145
+
146
+ def get_device_command_topic(
147
+ self, device_id: str, prefix: str = TOPIC_PREFIX
148
+ ) -> str:
149
+ """Return the 'set state' topic."""
150
+ return f"{prefix}/{device_id}/setState"
151
+
152
+ def get_output_command_topic(
153
+ self, device_id: str, entity_id: str, prefix: str = TOPIC_PREFIX
154
+ ) -> str:
155
+ """Return the 'set state' topic of an output."""
156
+ return f"{prefix}/{device_id}/{entity_id}/setState"
157
+
158
+ def get_output_state_topic(
159
+ self, device_id: str, entity_id: str, prefix: str = TOPIC_PREFIX
160
+ ) -> str:
161
+ """Return the state topic of an output."""
162
+ return f"{prefix}/{device_id}/{entity_id}/state"
163
+
164
+
165
+ class IgnoreNoneJsonEncoder(json.JSONEncoder):
166
+ """A json encoder to ignore None values when serializing."""
167
+
168
+ def default(self, o):
169
+ if hasattr(o, "__dict__"):
170
+ # Filter out None values
171
+ return {k: v for k, v in o.__dict__.items() if v is not None}
172
+ return super().default(o)
@@ -0,0 +1,140 @@
1
+ """Qbus state models."""
2
+
3
+ from enum import StrEnum
4
+ from typing import Any
5
+ from .const import (
6
+ KEY_OUTPUT_ACTION,
7
+ KEY_OUTPUT_ID,
8
+ KEY_OUTPUT_PROPERTIES,
9
+ KEY_OUTPUT_TYPE,
10
+ KEY_PROPERTIES_VALUE,
11
+ )
12
+
13
+ KEY_CONTROLLER_CONNECTABLE = "connectable"
14
+ KEY_CONTROLLER_CONNECTED = "connected"
15
+ KEY_CONTROLLER_ID = "id"
16
+ KEY_CONTROLLER_STATE_PROPERTIES = "properties"
17
+
18
+
19
+ class StateType(StrEnum):
20
+ """Values to be used as state type."""
21
+
22
+ ACTION = "action"
23
+ STATE = "state"
24
+
25
+
26
+ class StateAction(StrEnum):
27
+ """Values to be used as state action."""
28
+
29
+ ACTIVATE = "activate"
30
+ ACTIVE = "active"
31
+
32
+
33
+ class QbusMqttControllerStateProperties:
34
+ """MQTT representation a Qbus controller its state properties."""
35
+
36
+ def __init__(self, data: dict) -> None:
37
+ """Initialize based on a json loaded dictionary."""
38
+ self.connectable: bool | None = data.get(KEY_CONTROLLER_CONNECTABLE)
39
+ self.connected: bool | None = data.get(KEY_CONTROLLER_CONNECTED)
40
+
41
+
42
+ class QbusMqttControllerState:
43
+ """MQTT representation of a Qbus controller state."""
44
+
45
+ def __init__(self, data: dict) -> None:
46
+ """Initialize based on a json loaded dictionary."""
47
+ self.id: str | None = data.get(KEY_CONTROLLER_ID)
48
+
49
+ properties = data.get(KEY_CONTROLLER_STATE_PROPERTIES)
50
+ self.properties: QbusMqttControllerStateProperties | None = (
51
+ QbusMqttControllerStateProperties(properties)
52
+ if properties is not None
53
+ else None
54
+ )
55
+
56
+
57
+ class QbusMqttState:
58
+ """MQTT representation of a Qbus state."""
59
+
60
+ def __init__(
61
+ self,
62
+ data: dict | None = None,
63
+ *,
64
+ id: str | None = None,
65
+ type: str | None = None,
66
+ action: str | None = None,
67
+ ) -> None:
68
+ """Initialize state."""
69
+ self.id: str = ""
70
+ self.type: str = ""
71
+ self.action: str | None = None
72
+ self.properties: dict | None = None
73
+
74
+ if data is not None:
75
+ self.id = data.get(KEY_OUTPUT_ID, "")
76
+ self.type = data.get(KEY_OUTPUT_TYPE, "")
77
+ self.action = data.get(KEY_OUTPUT_ACTION)
78
+ self.properties = data.get(KEY_OUTPUT_PROPERTIES)
79
+
80
+ if id is not None:
81
+ self.id = id
82
+
83
+ if type is not None:
84
+ self.type = type
85
+
86
+ if action is not None:
87
+ self.action = action
88
+
89
+ def read_property(self, key: str, default: Any) -> Any:
90
+ """Read a property."""
91
+ return self.properties.get(key, default) if self.properties else default
92
+
93
+ def write_property(self, key: str, value: Any) -> None:
94
+ """Add or update a property."""
95
+ if self.properties is None:
96
+ self.properties = {}
97
+
98
+ self.properties[key] = value
99
+
100
+
101
+ class QbusMqttOnOffState(QbusMqttState):
102
+ """MQTT representation of a Qbus on/off output."""
103
+
104
+ def __init__(
105
+ self,
106
+ data: dict | None = None,
107
+ *,
108
+ id: str | None = None,
109
+ type: str | None = None,
110
+ ) -> None:
111
+ super().__init__(data, id=id, type=type)
112
+
113
+ def read_value(self) -> bool:
114
+ """Read the value of the Qbus output."""
115
+ return self.read_property(KEY_PROPERTIES_VALUE, False)
116
+
117
+ def write_value(self, value: bool) -> None:
118
+ """Set the value of the Qbus output."""
119
+ self.write_property(KEY_PROPERTIES_VALUE, value)
120
+
121
+
122
+ class QbusMqttAnalogState(QbusMqttState):
123
+ """MQTT representation of a Qbus analog output."""
124
+
125
+ def __init__(
126
+ self,
127
+ data: dict | None = None,
128
+ *,
129
+ id: str | None = None,
130
+ type: str | None = None,
131
+ ) -> None:
132
+ super().__init__(data, id=id, type=type)
133
+
134
+ def read_percentage(self) -> float:
135
+ """Read the value of the Qbus output."""
136
+ return self.read_property(KEY_PROPERTIES_VALUE, 0)
137
+
138
+ def write_percentage(self, percentage: float) -> None:
139
+ """Set the value of the Qbus output."""
140
+ self.write_property(KEY_PROPERTIES_VALUE, percentage)
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.1
2
+ Name: qbusmqttapi
3
+ Version: 0.2.7
4
+ Summary: MQTT API for Qbus Home Automation.
5
+ Home-page: https://github.com/Qbus-iot/qbusmqttapi
6
+ Author: Koen Schockaert
7
+ Author-email: ks@qbus.be
8
+ License: MIT License 2024
9
+ Project-URL: Source Code, https://github.com/Qbus-iot/qbusmqttapi
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+
17
+ # qbusmqttapi
18
+ Python MQTT API for Qbus Home Automation
@@ -0,0 +1,14 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ setup.cfg
5
+ setup.py
6
+ src/qbusmqttapi/__init__.py
7
+ src/qbusmqttapi/const.py
8
+ src/qbusmqttapi/discovery.py
9
+ src/qbusmqttapi/factory.py
10
+ src/qbusmqttapi/state.py
11
+ src/qbusmqttapi.egg-info/PKG-INFO
12
+ src/qbusmqttapi.egg-info/SOURCES.txt
13
+ src/qbusmqttapi.egg-info/dependency_links.txt
14
+ src/qbusmqttapi.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ qbusmqttapi