blebox-uniapi 2.5.5__tar.gz → 2.5.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.
Files changed (52) hide show
  1. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/CONTRIBUTING.rst +8 -4
  2. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/MANIFEST.in +0 -1
  3. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/PKG-INFO +17 -5
  4. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/blebox_uniapi/__init__.py +1 -1
  5. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/blebox_uniapi/box.py +6 -0
  6. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/blebox_uniapi/box_types.py +36 -4
  7. blebox_uniapi-2.5.7/blebox_uniapi/button.py +141 -0
  8. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/blebox_uniapi/climate.py +38 -7
  9. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/blebox_uniapi/cover.py +29 -2
  10. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/blebox_uniapi/error.py +0 -2
  11. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/blebox_uniapi/sensor.py +89 -20
  12. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/blebox_uniapi/session.py +2 -2
  13. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/blebox_uniapi.egg-info/PKG-INFO +18 -6
  14. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/blebox_uniapi.egg-info/SOURCES.txt +13 -1
  15. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/setup.cfg +2 -1
  16. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/setup.py +2 -1
  17. blebox_uniapi-2.5.7/tests/test_box.py +236 -0
  18. blebox_uniapi-2.5.7/tests/test_box_types.py +80 -0
  19. blebox_uniapi-2.5.7/tests/test_button.py +266 -0
  20. blebox_uniapi-2.5.7/tests/test_climate.py +424 -0
  21. blebox_uniapi-2.5.7/tests/test_cover.py +962 -0
  22. blebox_uniapi-2.5.7/tests/test_light.py +1369 -0
  23. blebox_uniapi-2.5.7/tests/test_light_unit.py +141 -0
  24. blebox_uniapi-2.5.7/tests/test_sensor.py +587 -0
  25. blebox_uniapi-2.5.7/tests/test_session.py +233 -0
  26. blebox_uniapi-2.5.7/tests/test_switch.py +447 -0
  27. blebox_uniapi-2.5.7/tests/test_unknown.py +59 -0
  28. blebox_uniapi-2.5.7/tests/test_update.py +70 -0
  29. blebox_uniapi-2.5.5/blebox_uniapi/button.py +0 -75
  30. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/HISTORY.rst +0 -0
  31. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/LICENSE +0 -0
  32. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/README.rst +0 -0
  33. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/blebox_uniapi/binary_sensor.py +0 -0
  34. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/blebox_uniapi/feature.py +0 -0
  35. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/blebox_uniapi/jfollow.py +0 -0
  36. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/blebox_uniapi/light.py +0 -0
  37. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/blebox_uniapi/switch.py +0 -0
  38. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/blebox_uniapi/update.py +0 -0
  39. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/blebox_uniapi.egg-info/dependency_links.txt +0 -0
  40. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/blebox_uniapi.egg-info/not-zip-safe +0 -0
  41. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/blebox_uniapi.egg-info/requires.txt +0 -0
  42. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/blebox_uniapi.egg-info/top_level.txt +0 -0
  43. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/docs/Makefile +0 -0
  44. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/docs/authors.rst +0 -0
  45. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/docs/conf.py +0 -0
  46. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/docs/contributing.rst +0 -0
  47. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/docs/history.rst +0 -0
  48. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/docs/index.rst +0 -0
  49. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/docs/installation.rst +0 -0
  50. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/docs/make.bat +0 -0
  51. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/docs/readme.rst +0 -0
  52. {blebox_uniapi-2.5.5 → blebox_uniapi-2.5.7}/docs/usage.rst +0 -0
@@ -122,7 +122,11 @@ Make sure all your changes are committed (including an entry in HISTORY.rst).
122
122
  Then run::
123
123
 
124
124
  $ bump2version patch # possible: major / minor / patch
125
- $ git push
126
- $ git push --tags
127
-
128
- Travis will then deploy to PyPI if tests pass.
125
+ $ git push --follow-tags
126
+
127
+ bump2version bumps the version in setup.py, blebox_uniapi/__init__.py and
128
+ setup.cfg, commits the change and creates a matching git tag (e.g. v2.6.0).
129
+ Pushing the tag triggers the "Publish to PyPI" GitHub Actions workflow
130
+ (.github/workflows/release.yml), which first runs lint + tests, then builds and
131
+ uploads to PyPI via Trusted Publishing (OIDC) from the blebox/blebox_uniapi repo.
132
+ The publish step only runs if the tests pass.
@@ -1,4 +1,3 @@
1
- include AUTHORS.rst
2
1
  include CONTRIBUTING.rst
3
2
  include HISTORY.rst
4
3
  include LICENSE
@@ -1,22 +1,36 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: blebox_uniapi
3
- Version: 2.5.5
3
+ Version: 2.5.7
4
4
  Summary: Python API for accessing BleBox smart home devices
5
5
  Home-page: https://github.com/blebox/blebox_uniapi
6
6
  Author: BleBox
7
7
  Author-email: opensource@blebox.eu
8
8
  License: Apache Software License 2.0
9
9
  Keywords: blebox_uniapi
10
- Platform: UNKNOWN
11
10
  Classifier: Development Status :: 2 - Pre-Alpha
12
11
  Classifier: Intended Audience :: Developers
13
12
  Classifier: License :: OSI Approved :: Apache Software License
14
13
  Classifier: Natural Language :: English
15
14
  Classifier: Programming Language :: Python :: 3.9
16
15
  Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
17
  Requires-Python: >=3.9
18
18
  Description-Content-Type: text/x-rst
19
19
  License-File: LICENSE
20
+ Requires-Dist: aiohttp>=3
21
+ Requires-Dist: jmespath>1.0.0
22
+ Dynamic: author
23
+ Dynamic: author-email
24
+ Dynamic: classifier
25
+ Dynamic: description
26
+ Dynamic: description-content-type
27
+ Dynamic: home-page
28
+ Dynamic: keywords
29
+ Dynamic: license
30
+ Dynamic: license-file
31
+ Dynamic: requires-dist
32
+ Dynamic: requires-python
33
+ Dynamic: summary
20
34
 
21
35
  ====================
22
36
  BleBox Python UniAPI
@@ -227,5 +241,3 @@ History
227
241
  ------------------
228
242
 
229
243
  * First release on PyPI.
230
-
231
-
@@ -2,4 +2,4 @@
2
2
 
3
3
  __author__ = """BleBox"""
4
4
  __email__ = "opensource@blebox.eu"
5
- __version__ = "2.5.5"
5
+ __version__ = "2.5.7"
@@ -111,6 +111,7 @@ class Box:
111
111
 
112
112
  self._data_path = config["api_path"]
113
113
  self._type = type
114
+ self._product = product
114
115
  self._unique_id = unique_id
115
116
  self._name = name
116
117
  self._address = address
@@ -212,6 +213,11 @@ class Box:
212
213
  def type(self) -> str:
213
214
  return self._type
214
215
 
216
+ @property
217
+ def product(self) -> str:
218
+ """Product name for informational purpose only - do not use it in any application logic."""
219
+ return self._product
220
+
215
221
  @property
216
222
  def unique_id(self) -> Any:
217
223
  return self._unique_id
@@ -1,3 +1,4 @@
1
+ from .button import GateBoxSecondOutput, TvLift
1
2
  from .cover import Gate, GateBox, GateBoxB, Shutter
2
3
  from typing import Union, Any
3
4
 
@@ -50,9 +51,8 @@ BOX_TYPE_CONF: dict[str, dict[int, dict[str, Any]]] = {
50
51
  "set": lambda command: ("GET", f"/s/c/{command}")
51
52
  }, # dictionary with interaction methods
52
53
  "buttons": [
53
- "tvLift",
54
- {"lift": ""},
55
- ], # key used to set platform, list elements used in cls init, e.g. [<alias>, {"path": "state_value"}]
54
+ ["tvLift", {}, TvLift],
55
+ ], # key used to set platform, list elements used in cls init, e.g. [<alias>, {"path": "state_value"}, <optional type class>]
56
56
  }
57
57
  },
58
58
  # airSensor
@@ -135,11 +135,15 @@ BOX_TYPE_CONF: dict[str, dict[int, dict[str, Any]]] = {
135
135
  {
136
136
  "position": "gate.currentPos",
137
137
  "gate_type": "gate.gateType",
138
+ "extra_button_type": "gate.extraButtonType",
138
139
  },
139
140
  "gatebox",
140
141
  GateBoxB,
141
142
  ]
142
143
  ],
144
+ "buttons": [
145
+ ["second_output", {}, GateBoxSecondOutput],
146
+ ],
143
147
  },
144
148
  },
145
149
  # gateController
@@ -187,6 +191,7 @@ BOX_TYPE_CONF: dict[str, dict[int, dict[str, Any]]] = {
187
191
  "minimum": "thermo.minimumTemp",
188
192
  "maximum": "thermo.maximumTemp",
189
193
  "temperature": lambda x: f"sensors[?id == `{x}`]|[0]|value",
194
+ "temperature.state": lambda x: f"sensors[?id == `{x}`]|[0]|state",
190
195
  "state": "thermo.state",
191
196
  "mode": "thermo.mode",
192
197
  "safetySensorId": "thermo.safetyTempSensor.sensorId",
@@ -199,6 +204,7 @@ BOX_TYPE_CONF: dict[str, dict[int, dict[str, Any]]] = {
199
204
  "temperature",
200
205
  {
201
206
  "temperature": lambda x: f"sensors[?id == `{x}`]|[0]|value",
207
+ "temperature.state": lambda x: f"sensors[?id == `{x}`]|[0]|state",
202
208
  },
203
209
  ]
204
210
  ],
@@ -526,7 +532,7 @@ BOX_TYPE_CONF: dict[str, dict[int, dict[str, Any]]] = {
526
532
  {
527
533
  "temperature": "tempSensor.sensors[?id == `0`]|[0]|value",
528
534
  "trend": "tempSensor.sensors[?id == `0`]|[0]|trend",
529
- "state": "tempSensor.sensors[?id == `0`]|[0]|state",
535
+ "temperature.state": "tempSensor.sensors[?id == `0`]|[0]|state",
530
536
  "elapsed": "tempSensor.sensors[?id == `0`]|[0]|elapsedTimeS",
531
537
  },
532
538
  ]
@@ -678,7 +684,9 @@ BOX_TYPE_CONF: dict[str, dict[int, dict[str, Any]]] = {
678
684
  "multiSensor",
679
685
  {
680
686
  "temperature": lambda x: f"multiSensor.sensors[?id == `{x}`]|[0]|value",
687
+ "temperature.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[0]|state",
681
688
  "wind": lambda x: f"multiSensor.sensors[?id == `{x}`]|[0]|value",
689
+ "wind.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[0]|state",
682
690
  },
683
691
  ]
684
692
  ],
@@ -700,8 +708,11 @@ BOX_TYPE_CONF: dict[str, dict[int, dict[str, Any]]] = {
700
708
  "multiSensor",
701
709
  {
702
710
  "temperature": lambda x: f"multiSensor.sensors[?id == `{x}`]|[0]|value",
711
+ "temperature.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[0]|state",
703
712
  "wind": lambda x: f"multiSensor.sensors[?id == `{x}`]|[0]|value",
713
+ "wind.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[0]|state",
704
714
  "humidity": lambda x: f"multiSensor.sensors[?id == `{x}`]|[0]|value",
715
+ "humidity.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[0]|state",
705
716
  },
706
717
  ]
707
718
  ],
@@ -723,9 +734,13 @@ BOX_TYPE_CONF: dict[str, dict[int, dict[str, Any]]] = {
723
734
  "multiSensor",
724
735
  {
725
736
  "illuminance": lambda x: f"multiSensor.sensors[?id == `{x}`]|[0]|value",
737
+ "illuminance.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[0]|state",
726
738
  "temperature": lambda x: f"multiSensor.sensors[?id == `{x}`]|[0]|value",
739
+ "temperature.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[0]|state",
727
740
  "wind": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'wind']|[0]|value",
741
+ "wind.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'wind']|[0]|state",
728
742
  "humidity": lambda x: f"multiSensor.sensors[?id == `{x}`]|[0]|value",
743
+ "humidity.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[0]|state",
729
744
  },
730
745
  ]
731
746
  ],
@@ -747,22 +762,39 @@ BOX_TYPE_CONF: dict[str, dict[int, dict[str, Any]]] = {
747
762
  "multiSensor",
748
763
  {
749
764
  "frequency": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'frequency']|[0]|value",
765
+ "frequency.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'frequency']|[0]|state",
750
766
  "current": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'current']|[0]|value",
767
+ "current.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'current']|[0]|state",
751
768
  "voltage": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'voltage']|[0]|value",
769
+ "voltage.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'voltage']|[0]|state",
752
770
  "apparentPower": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'apparentPower']|[0]|value",
771
+ "apparentPower.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'apparentPower']|[0]|state",
753
772
  "activePower": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'activePower']|[0]|value",
773
+ "activePower.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'activePower']|[0]|state",
754
774
  "reactivePower": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'reactivePower']|[0]|value",
775
+ "reactivePower.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'reactivePower']|[0]|state",
755
776
  "reverseActiveEnergy": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'reverseActiveEnergy']|[0]|value",
777
+ "reverseActiveEnergy.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'reverseActiveEnergy']|[0]|state",
756
778
  "forwardActiveEnergy": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'forwardActiveEnergy']|[0]|value",
779
+ "forwardActiveEnergy.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'forwardActiveEnergy']|[0]|state",
757
780
  "forwardReactiveEnergy": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'forwardReactiveEnergy']|[0]|value",
781
+ "forwardReactiveEnergy.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'forwardReactiveEnergy']|[0]|state",
758
782
  "reverseReactiveEnergy": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'reverseReactiveEnergy']|[0]|value",
783
+ "reverseReactiveEnergy.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'reverseReactiveEnergy']|[0]|state",
759
784
  "illuminance": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'illuminance']|[0]|value",
785
+ "illuminance.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'illuminance']|[0]|state",
760
786
  "temperature": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'temperature']|[0]|value",
787
+ "temperature.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'temperature']|[0]|state",
761
788
  "wind": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'wind']|[0]|value",
789
+ "wind.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'wind']|[0]|state",
762
790
  "humidity": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'humidity']|[0]|value",
791
+ "humidity.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'humidity']|[0]|state",
763
792
  "openStatus": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'openStatus']|[0]|value",
793
+ "openStatus.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'openStatus']|[0]|state",
764
794
  "co2": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'co2']|[0]|value",
795
+ "co2.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'co2']|[0]|state",
765
796
  "co2Definition": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'co2Definition']|[0]|value",
797
+ "co2Definition.state": lambda x: f"multiSensor.sensors[?id == `{x}`]|[?type == 'co2Definition']|[0]|state",
766
798
  },
767
799
  ]
768
800
  ],
@@ -0,0 +1,141 @@
1
+ from .cover import GateBoxControlType, GateBoxExtraButtonType
2
+ from .feature import Feature
3
+ from typing import TYPE_CHECKING, Any, Optional, Type
4
+
5
+ from enum import Enum, auto
6
+
7
+ if TYPE_CHECKING:
8
+ from .box import Box
9
+
10
+ TV_LIFT_CONTROL_TYPES_API = {
11
+ 0: {"1": "open_or_stop", "2": "close_or_stop"},
12
+ 1: {"1": "up_or_stop", "2": "down_or_stop"},
13
+ 2: {"1": "up_or_stop", "2": "down_or_stop"},
14
+ 3: {"1": "up_or_stop", "2": "down_or_stop"},
15
+ 4: {"1": "open_or_stop", "2": "close_or_stop", "3": "to_fav"},
16
+ }
17
+
18
+
19
+ class ControlType(Enum):
20
+ UP = auto()
21
+ DOWN = auto()
22
+ FAVORITE = auto()
23
+ OPEN = auto()
24
+ CLOSE = auto()
25
+
26
+
27
+ class Buttons:
28
+ """Buttons defines how a product derives its buttons from extended state."""
29
+
30
+ @staticmethod
31
+ def many_from_extended_state(
32
+ button_cls: Type["Button"],
33
+ product: "Box",
34
+ alias: str,
35
+ methods: dict,
36
+ extended_state: dict,
37
+ ) -> list["Button"]:
38
+ raise NotImplementedError # pragma: no cover
39
+
40
+
41
+ class TvLift(Buttons):
42
+ @staticmethod
43
+ def many_from_extended_state(
44
+ button_cls: Type["Button"],
45
+ product: "Box",
46
+ alias: str,
47
+ methods: dict,
48
+ extended_state: dict,
49
+ ) -> list["Button"]:
50
+ control_type = extended_state.get("tvLift", {}).get("controlType")
51
+ endpoints = TV_LIFT_CONTROL_TYPES_API.get(control_type, {})
52
+ return [
53
+ button_cls(product, f"{alias}_{endpoint}", methods, endpoint)
54
+ for endpoint in endpoints.values()
55
+ ]
56
+
57
+
58
+ class GateBoxSecondOutput(Buttons):
59
+ """GateBoxSecondOutput exposes the gateBox extra button output as a button.
60
+
61
+ WALK_IN and OTHER share one button name because what the output does is a
62
+ device setting the user can change at any time, not a hardware trait.
63
+ """
64
+
65
+ @staticmethod
66
+ def many_from_extended_state(
67
+ button_cls: Type["Button"],
68
+ product: "Box",
69
+ alias: str,
70
+ methods: dict,
71
+ extended_state: dict,
72
+ ) -> list["Button"]:
73
+ gate = extended_state.get("gate", {})
74
+ if gate.get("extraButtonType") not in (
75
+ GateBoxExtraButtonType.WALK_IN,
76
+ GateBoxExtraButtonType.OTHER,
77
+ ):
78
+ return []
79
+
80
+ # Api levels below 20230102 report no mode and cannot be wired that way.
81
+ if gate.get("openCloseMode") == GateBoxControlType.OPEN_CLOSE:
82
+ return []
83
+
84
+ return [button_cls(product, alias, methods, "second_output", "secondary")]
85
+
86
+
87
+ class Button(Feature):
88
+ def __init__(
89
+ self,
90
+ product: "Box",
91
+ alias: str,
92
+ methods: dict,
93
+ query_string: str,
94
+ api_command: str = "set",
95
+ ) -> None:
96
+ super().__init__(product, alias, methods)
97
+ self._device_class = "UPDATE"
98
+ self._query_string: str = query_string
99
+ self._api_command: str = api_command
100
+
101
+ @classmethod
102
+ def many_from_config(
103
+ cls, product: "Box", box_type_config: list, extended_state: Any
104
+ ) -> list["Button"]:
105
+ if not isinstance(extended_state, dict):
106
+ return []
107
+
108
+ features: list[Button] = []
109
+ for alias, methods, buttons in box_type_config:
110
+ features.extend(
111
+ buttons.many_from_extended_state(
112
+ cls, product, alias, methods, extended_state
113
+ )
114
+ )
115
+ return features
116
+
117
+ async def set(self) -> None:
118
+ await self.async_api_command(self._api_command, self.query_string)
119
+
120
+ def after_update(self) -> None:
121
+ pass
122
+
123
+ @property
124
+ def control_type(self) -> Optional[ControlType]:
125
+ """Return icon for endpoint."""
126
+ if "up" in self.query_string:
127
+ return ControlType.UP
128
+ elif "down" in self.query_string:
129
+ return ControlType.DOWN
130
+ elif "fav" in self.query_string:
131
+ return ControlType.FAVORITE
132
+ elif "open" in self.query_string:
133
+ return ControlType.OPEN
134
+ elif "close" in self.query_string:
135
+ return ControlType.CLOSE
136
+ else:
137
+ return None
138
+
139
+ @property
140
+ def query_string(self) -> str:
141
+ return self._query_string
@@ -1,9 +1,19 @@
1
+ from enum import IntEnum
1
2
  from .sensor import Temperature
2
3
  from typing import Optional, Any, Union
3
4
  from .feature import Feature
4
5
  from blebox_uniapi.jfollow import follow
5
6
 
6
7
 
8
+ class BleboxThermoState(IntEnum):
9
+ """Possible states of a thermo regulator, as reported by the device."""
10
+
11
+ OFF = 0
12
+ ON = 1
13
+ BOOST = 3
14
+ SAFETY_OFF = 4
15
+
16
+
7
17
  class Climate(Temperature):
8
18
  _is_on: Optional[bool]
9
19
  _desired: Union[float, int, None]
@@ -90,12 +100,18 @@ class Climate(Temperature):
90
100
  raw = self.raw_value("state")
91
101
  if raw is not None: # no reading
92
102
  alias = self._alias
93
- return product.expect_int(alias, raw, 3, 0) in (
94
- 1,
95
- 3,
96
- ) # 1: On, 3: Boost(thermoBox max temp of mode)
103
+ return product.expect_int(alias, raw, max(BleboxThermoState), 0) in (
104
+ BleboxThermoState.ON,
105
+ BleboxThermoState.BOOST,
106
+ )
97
107
  return None
98
108
 
109
+ def _read_safety_off(self) -> bool:
110
+ """Return True when the safety system forced the output off."""
111
+ if self._product.last_data is None:
112
+ return False
113
+ return self.raw_value("state") == BleboxThermoState.SAFETY_OFF
114
+
99
115
  def _read_operating_state(self) -> Optional[int]:
100
116
  """Return current operating state"""
101
117
  if self._product.last_data is not None:
@@ -107,13 +123,13 @@ class Climate(Temperature):
107
123
  return None
108
124
 
109
125
  def _read_is_heating(self) -> Optional[bool]:
110
- if not self._product.last_data:
126
+ if not self._product.last_data or self.current is None or self.desired is None:
111
127
  return None
112
128
 
113
129
  return self.is_on and (self.current < self.desired)
114
130
 
115
131
  def _read_is_cooling(self) -> Optional[bool]:
116
- if not self._product.last_data:
132
+ if not self._product.last_data or self.current is None or self.desired is None:
117
133
  return None
118
134
 
119
135
  return self.is_on and (self.current > self.desired)
@@ -131,7 +147,22 @@ class Climate(Temperature):
131
147
  def after_update(self) -> None:
132
148
  self._is_on = self._read_is_on()
133
149
  self._desired = self._read_temperature("desired")
134
- self._current = self._read_temperature("temperature")
150
+
151
+ state = self._read_state("temperature")
152
+ if self._state_is_error(state):
153
+ self._error = True
154
+ self._current = None
155
+ else:
156
+ self._error = False
157
+ if self._state_is_initializing(state):
158
+ self._current = None
159
+ else:
160
+ self._current = self._read_temperature("temperature")
161
+
162
+ if self._read_safety_off():
163
+ self._error = True
164
+
165
+ self._native_value = self._current
135
166
  self._is_heating = self._read_is_heating()
136
167
  self._is_cooling = self._read_is_cooling()
137
168
  self._havc_action = self._read_operating_state()
@@ -65,6 +65,19 @@ class GateBoxControlType(IntEnum):
65
65
  OPEN_CLOSE = 2
66
66
 
67
67
 
68
+ class GateBoxExtraButtonType(IntEnum):
69
+ """GateBoxExtraButtonType defines gateBox extra button semantics.
70
+
71
+ Only meaningful outside OPEN_CLOSE mode, which takes the extra button output
72
+ for the close action and leaves this field unused.
73
+ """
74
+
75
+ DISABLED = 0
76
+ STOP = 1
77
+ WALK_IN = 2
78
+ OTHER = 3
79
+
80
+
68
81
  class GateBoxGateType(IntEnum):
69
82
  """GateBoxGateType defines possible gate/cover types reported by gateBox"""
70
83
 
@@ -305,7 +318,7 @@ class GateBox(Gate):
305
318
  if button_type is None:
306
319
  return False
307
320
 
308
- return button_type == 1
321
+ return button_type == GateBoxExtraButtonType.STOP
309
322
 
310
323
 
311
324
  class GateBoxB(GateBox):
@@ -328,9 +341,23 @@ class GateBoxB(GateBox):
328
341
  return raw_value("position")
329
342
 
330
343
  def read_has_stop(self, alias: str, raw_value: Any, product: "Box") -> bool:
344
+ if product.last_data is None:
345
+ return False
346
+
331
347
  # note: if control type is unknown we assume it is not open/close
332
348
  # and has the stop feature via secondary button command.
333
- return self._control_type != GateBoxControlType.OPEN_CLOSE
349
+ if self._control_type == GateBoxControlType.OPEN_CLOSE:
350
+ return False
351
+
352
+ # stop_command issues the same secondary command as the second_output button.
353
+ extra_button_type = raw_value("extra_button_type")
354
+ if extra_button_type in (
355
+ GateBoxExtraButtonType.WALK_IN,
356
+ GateBoxExtraButtonType.OTHER,
357
+ ):
358
+ return False
359
+
360
+ return True
334
361
 
335
362
  def read_cover_type(
336
363
  self, alias: str, raw_value: Any, product: "Box"
@@ -39,7 +39,6 @@ class UnsupportedBoxVersion(BoxError):
39
39
  pass
40
40
 
41
41
 
42
-
43
42
  class BadFieldExceedsMax(BoxError):
44
43
  def __init__(self, dev_name: str, field: str, value: int, max_value: int):
45
44
  self._dev_name = dev_name
@@ -103,7 +102,6 @@ class BadFieldNotRGBW(BoxError):
103
102
  return f"{self._dev_name}.{self._field} is {self._value} which is not a rgbw string"
104
103
 
105
104
 
106
-
107
105
  # misc errors
108
106
 
109
107