ramses-rf 0.22.40__py3-none-any.whl → 0.51.2__py3-none-any.whl

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 (71) hide show
  1. ramses_cli/__init__.py +18 -0
  2. ramses_cli/client.py +597 -0
  3. ramses_cli/debug.py +20 -0
  4. ramses_cli/discovery.py +405 -0
  5. ramses_cli/utils/cat_slow.py +17 -0
  6. ramses_cli/utils/convert.py +60 -0
  7. ramses_rf/__init__.py +31 -10
  8. ramses_rf/binding_fsm.py +787 -0
  9. ramses_rf/const.py +124 -105
  10. ramses_rf/database.py +297 -0
  11. ramses_rf/device/__init__.py +69 -39
  12. ramses_rf/device/base.py +187 -376
  13. ramses_rf/device/heat.py +540 -552
  14. ramses_rf/device/hvac.py +279 -171
  15. ramses_rf/dispatcher.py +153 -177
  16. ramses_rf/entity_base.py +478 -361
  17. ramses_rf/exceptions.py +82 -0
  18. ramses_rf/gateway.py +377 -513
  19. ramses_rf/helpers.py +57 -19
  20. ramses_rf/py.typed +0 -0
  21. ramses_rf/schemas.py +148 -194
  22. ramses_rf/system/__init__.py +16 -23
  23. ramses_rf/system/faultlog.py +363 -0
  24. ramses_rf/system/heat.py +295 -302
  25. ramses_rf/system/schedule.py +312 -198
  26. ramses_rf/system/zones.py +318 -238
  27. ramses_rf/version.py +2 -8
  28. ramses_rf-0.51.2.dist-info/METADATA +72 -0
  29. ramses_rf-0.51.2.dist-info/RECORD +55 -0
  30. {ramses_rf-0.22.40.dist-info → ramses_rf-0.51.2.dist-info}/WHEEL +1 -2
  31. ramses_rf-0.51.2.dist-info/entry_points.txt +2 -0
  32. {ramses_rf-0.22.40.dist-info → ramses_rf-0.51.2.dist-info/licenses}/LICENSE +1 -1
  33. ramses_tx/__init__.py +160 -0
  34. {ramses_rf/protocol → ramses_tx}/address.py +65 -59
  35. ramses_tx/command.py +1454 -0
  36. ramses_tx/const.py +903 -0
  37. ramses_tx/exceptions.py +92 -0
  38. {ramses_rf/protocol → ramses_tx}/fingerprints.py +56 -15
  39. {ramses_rf/protocol → ramses_tx}/frame.py +132 -131
  40. ramses_tx/gateway.py +338 -0
  41. ramses_tx/helpers.py +883 -0
  42. {ramses_rf/protocol → ramses_tx}/logger.py +67 -53
  43. {ramses_rf/protocol → ramses_tx}/message.py +155 -191
  44. ramses_tx/opentherm.py +1260 -0
  45. ramses_tx/packet.py +210 -0
  46. {ramses_rf/protocol → ramses_tx}/parsers.py +1266 -1003
  47. ramses_tx/protocol.py +801 -0
  48. ramses_tx/protocol_fsm.py +672 -0
  49. ramses_tx/py.typed +0 -0
  50. {ramses_rf/protocol → ramses_tx}/ramses.py +262 -185
  51. {ramses_rf/protocol → ramses_tx}/schemas.py +150 -133
  52. ramses_tx/transport.py +1471 -0
  53. ramses_tx/typed_dicts.py +492 -0
  54. ramses_tx/typing.py +181 -0
  55. ramses_tx/version.py +4 -0
  56. ramses_rf/discovery.py +0 -398
  57. ramses_rf/protocol/__init__.py +0 -59
  58. ramses_rf/protocol/backports.py +0 -42
  59. ramses_rf/protocol/command.py +0 -1576
  60. ramses_rf/protocol/const.py +0 -697
  61. ramses_rf/protocol/exceptions.py +0 -111
  62. ramses_rf/protocol/helpers.py +0 -390
  63. ramses_rf/protocol/opentherm.py +0 -1170
  64. ramses_rf/protocol/packet.py +0 -235
  65. ramses_rf/protocol/protocol.py +0 -613
  66. ramses_rf/protocol/transport.py +0 -1011
  67. ramses_rf/protocol/version.py +0 -10
  68. ramses_rf/system/hvac.py +0 -82
  69. ramses_rf-0.22.40.dist-info/METADATA +0 -64
  70. ramses_rf-0.22.40.dist-info/RECORD +0 -42
  71. ramses_rf-0.22.40.dist-info/top_level.txt +0 -1
ramses_rf/discovery.py DELETED
@@ -1,398 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- #
4
- """RAMSES RF - discovery scripts."""
5
- from __future__ import annotations
6
-
7
- import asyncio
8
- import json
9
- import logging
10
- import re
11
-
12
- from .const import SZ_SCHEDULE, SZ_ZONE_IDX, __dev_mode__
13
- from .protocol import CODES_SCHEMA, Command, ExpiredCallbackError, Priority
14
- from .protocol.command import _mk_cmd
15
- from .protocol.const import SZ_DISABLE_BACKOFF, SZ_PRIORITY, SZ_RETRIES
16
- from .protocol.opentherm import OTB_MSG_IDS
17
-
18
- # Beware, none of this is reliable - it is all subject to random change
19
- # However, these serve as examples how to us eteh other modules
20
-
21
-
22
- # skipcq: PY-W2000
23
- from .protocol import ( # noqa: F401, isort: skip, pylint: disable=unused-import
24
- I_,
25
- RP,
26
- RQ,
27
- W_,
28
- Code,
29
- )
30
-
31
-
32
- EXEC_CMD = "exec_cmd"
33
- GET_FAULTS = "get_faults"
34
- GET_SCHED = "get_schedule"
35
- SET_SCHED = "set_schedule"
36
-
37
- EXEC_SCR = "exec_scr"
38
- SCAN_DISC = "scan_disc"
39
- SCAN_FULL = "scan_full"
40
- SCAN_HARD = "scan_hard"
41
- SCAN_XXXX = "scan_xxxx"
42
-
43
- # DEVICE_ID_REGEX = re.compile(DEVICE_ID_REGEX.ANY)
44
-
45
- QOS_SCAN = {SZ_PRIORITY: Priority.LOW, SZ_RETRIES: 0}
46
- QOS_HIGH = {SZ_PRIORITY: Priority.HIGH, SZ_RETRIES: 3}
47
-
48
- DEV_MODE = __dev_mode__ and False
49
-
50
- _LOGGER = logging.getLogger(__name__)
51
- if DEV_MODE:
52
- _LOGGER.setLevel(logging.DEBUG)
53
-
54
-
55
- def script_decorator(fnc):
56
- def wrapper(gwy, *args, **kwargs):
57
-
58
- highest = {
59
- SZ_PRIORITY: Priority.HIGHEST,
60
- SZ_RETRIES: 3,
61
- SZ_DISABLE_BACKOFF: True,
62
- }
63
- gwy.send_cmd(Command._puzzle(message="Script begins:", qos=highest))
64
-
65
- result = fnc(gwy, *args, **kwargs)
66
-
67
- lowest = {SZ_PRIORITY: Priority.LOWEST, SZ_RETRIES: 3, SZ_DISABLE_BACKOFF: True}
68
- gwy.send_cmd(Command._puzzle(message="Script done.", qos=lowest))
69
-
70
- return result
71
-
72
- return wrapper
73
-
74
-
75
- async def periodic(gwy, cmd, count=1, interval=None):
76
- async def _periodic():
77
- await asyncio.sleep(interval)
78
- gwy.send_cmd(cmd)
79
-
80
- if interval is None:
81
- interval = 0 if count == 1 else 60
82
-
83
- if count <= 0:
84
- while True:
85
- await _periodic()
86
- else:
87
- for _ in range(count):
88
- await _periodic()
89
-
90
-
91
- def spawn_scripts(gwy, **kwargs) -> list[asyncio.Task]:
92
-
93
- tasks = []
94
-
95
- if kwargs.get(EXEC_CMD):
96
- tasks += [gwy._loop.create_task(exec_cmd(gwy, **kwargs))]
97
-
98
- if kwargs.get(GET_FAULTS):
99
- tasks += [gwy._loop.create_task(get_faults(gwy, kwargs[GET_FAULTS]))]
100
-
101
- elif kwargs.get(GET_SCHED) and kwargs[GET_SCHED][0]:
102
- tasks += [gwy._loop.create_task(get_schedule(gwy, *kwargs[GET_SCHED]))]
103
-
104
- elif kwargs.get(SET_SCHED) and kwargs[SET_SCHED][0]:
105
- tasks += [gwy._loop.create_task(set_schedule(gwy, *kwargs[SET_SCHED]))]
106
-
107
- elif kwargs.get(EXEC_SCR):
108
- script = SCRIPTS.get(f"{kwargs[EXEC_SCR][0]}")
109
- if script is None:
110
- _LOGGER.warning(f"Script: {kwargs[EXEC_SCR][0]}() - unknown script")
111
- else:
112
- _LOGGER.info(f"Script: {kwargs[EXEC_SCR][0]}().- starts...")
113
- tasks += [gwy._loop.create_task(script(gwy, kwargs[EXEC_SCR][1]))]
114
-
115
- gwy._tasks.extend(tasks)
116
- return tasks
117
-
118
-
119
- async def exec_cmd(gwy, **kwargs):
120
-
121
- await gwy.async_send_cmd(Command.from_cli(kwargs[EXEC_CMD], qos=QOS_HIGH))
122
-
123
-
124
- # @script_decorator
125
- # async def script_scan_001(gwy, dev_id: str):
126
- # _LOGGER.warning("scan_001() invoked - expect a lot of nonsense")
127
- # qos = {SZ_PRIORITY: Priority.LOW, SZ_RETRIES: 3}
128
- # for idx in range(0x10):
129
- # gwy.send_cmd(_mk_cmd(W_, Code._000E, f"{idx:02X}0050", dev_id, qos=qos))
130
- # gwy.send_cmd(_mk_cmd(RQ, Code._000E, f"{idx:02X}00C8", dev_id, qos=qos))
131
-
132
- # @script_decorator
133
- # async def script_scan_004(gwy, dev_id: str):
134
- # _LOGGER.warning("scan_004() invoked - expect a lot of nonsense")
135
- # cmd = Command.get_dhw_mode(dev_id, **QOS_SCAN)
136
- # return gwy._loop.create_task(periodic(gwy, cmd, count=0, interval=5))
137
-
138
-
139
- async def get_faults(gwy, ctl_id: str, start=0, limit=0x3F):
140
- ctl = gwy.get_device(ctl_id)
141
-
142
- try:
143
- await ctl.tcs.get_faultlog(start=start, limit=limit) # 0418
144
- except ExpiredCallbackError as exc:
145
- _LOGGER.error("get_faults(): Function timed out: %s", exc)
146
-
147
-
148
- async def get_schedule(gwy, ctl_id: str, zone_idx: str) -> None:
149
- zone = gwy.get_device(ctl_id).tcs.get_htg_zone(zone_idx)
150
-
151
- try:
152
- await zone.get_schedule()
153
- except ExpiredCallbackError as exc:
154
- _LOGGER.error("get_schedule(): Function timed out: %s", exc)
155
-
156
-
157
- async def set_schedule(gwy, ctl_id, schedule) -> None:
158
- schedule = json.load(schedule)
159
- zone_idx = schedule[SZ_ZONE_IDX]
160
-
161
- zone = gwy.get_device(ctl_id).tcs.get_htg_zone(zone_idx)
162
-
163
- try:
164
- await zone.set_schedule(schedule[SZ_SCHEDULE]) # 0404
165
- except ExpiredCallbackError as exc:
166
- _LOGGER.error("set_schedule(): Function timed out: %s", exc)
167
-
168
-
169
- async def script_bind_req(gwy, dev_id: str):
170
- gwy.get_device(dev_id)._make_fake(bind=True)
171
-
172
-
173
- async def script_bind_wait(gwy, dev_id: str, code=Code._2309, idx="00"):
174
- gwy.get_device(dev_id)._make_fake(bind=True, code=code, idx=idx)
175
-
176
-
177
- def script_poll_device(gwy, dev_id) -> list:
178
- _LOGGER.warning("poll_device() invoked...")
179
-
180
- tasks = []
181
-
182
- for code in (Code._0016, Code._1FC9):
183
- cmd = _mk_cmd(RQ, code, "00", dev_id, qos=QOS_SCAN)
184
- tasks.append(gwy._loop.create_task(periodic(gwy, cmd, count=0)))
185
-
186
- gwy._tasks.extend(tasks)
187
- return tasks
188
-
189
-
190
- @script_decorator
191
- async def script_scan_disc(gwy, dev_id: str):
192
- _LOGGER.warning("scan_disc() invoked...")
193
-
194
- await gwy.get_device(dev_id).discover() # discover_flag=Discover.DEFAULT)
195
-
196
-
197
- @script_decorator
198
- async def script_scan_full(gwy, dev_id: str):
199
- _LOGGER.warning("scan_full() invoked - expect a lot of Warnings")
200
-
201
- qos = {SZ_PRIORITY: Priority.DEFAULT, SZ_RETRIES: 5}
202
- gwy.send_cmd(_mk_cmd(RQ, Code._0016, "0000", dev_id, qos=qos))
203
-
204
- qos = {SZ_PRIORITY: Priority.DEFAULT, SZ_RETRIES: 1}
205
- for code in sorted(CODES_SCHEMA):
206
- if code == Code._0005:
207
- for zone_type in range(20): # known up to 18
208
- gwy.send_cmd(_mk_cmd(RQ, code, f"00{zone_type:02X}", dev_id, qos=qos))
209
-
210
- elif code == Code._000C:
211
- for zone_idx in range(16): # also: FA-FF?
212
- gwy.send_cmd(_mk_cmd(RQ, code, f"{zone_idx:02X}00", dev_id, qos=qos))
213
-
214
- elif code == Code._0016:
215
- continue
216
-
217
- elif code in (Code._01D0, Code._01E9):
218
- for zone_idx in ("00", "01", "FC"): # type: ignore[assignment]
219
- gwy.send_cmd(_mk_cmd(W_, code, f"{zone_idx}00", dev_id, qos=qos))
220
- gwy.send_cmd(_mk_cmd(W_, code, f"{zone_idx}03", dev_id, qos=qos))
221
-
222
- elif code == Code._0404: # FIXME
223
- gwy.send_cmd(Command.get_schedule_fragment(dev_id, "HW", 1, 0, qos=qos))
224
- gwy.send_cmd(Command.get_schedule_fragment(dev_id, "00", 1, 0, qos=qos))
225
-
226
- elif code == Code._0418:
227
- for log_idx in range(2):
228
- gwy.send_cmd(Command.get_system_log_entry(dev_id, log_idx, qos=qos))
229
-
230
- elif code == Code._1100:
231
- gwy.send_cmd(Command.get_tpi_params(dev_id, qos=qos))
232
-
233
- elif code == Code._2E04:
234
- gwy.send_cmd(Command.get_system_mode(dev_id, qos=qos))
235
-
236
- elif code == Code._3220:
237
- for data_id in (0, 3): # these are mandatory READ_DATA data_ids
238
- gwy.send_cmd(Command.get_opentherm_data(dev_id, data_id, qos=qos))
239
-
240
- elif code == Code._PUZZ:
241
- continue
242
-
243
- elif (
244
- code in CODES_SCHEMA
245
- and RQ in CODES_SCHEMA[code]
246
- and re.match(CODES_SCHEMA[code][RQ], "00")
247
- ):
248
- gwy.send_cmd(_mk_cmd(RQ, code, "00", dev_id, qos=qos))
249
-
250
- else:
251
- gwy.send_cmd(_mk_cmd(RQ, code, "0000", dev_id, qos=qos))
252
-
253
- # these are possible/difficult codes
254
- qos = {SZ_PRIORITY: Priority.DEFAULT, SZ_RETRIES: 2}
255
- for code in (Code._0150, Code._2389):
256
- gwy.send_cmd(_mk_cmd(RQ, code, "0000", dev_id, qos=qos))
257
-
258
-
259
- @script_decorator
260
- async def script_scan_hard(gwy, dev_id: str, *, start_code: int = None):
261
- _LOGGER.warning("scan_hard() invoked - expect some Warnings")
262
-
263
- start_code = start_code or 0
264
-
265
- for code in range(start_code, 0x5000):
266
- gwy.send_cmd(_mk_cmd(RQ, f"{code:04X}", "0000", dev_id, qos=QOS_SCAN))
267
- await asyncio.sleep(0.2)
268
-
269
-
270
- @script_decorator
271
- async def script_scan_fan(gwy, dev_id: str):
272
- _LOGGER.warning("scan_fan() invoked - expect a lot of nonsense")
273
- qos = {SZ_PRIORITY: Priority.LOW, SZ_RETRIES: 3}
274
-
275
- from ramses_rf.protocol.ramses import _DEV_KLASSES_HVAC
276
-
277
- OUT_CODES = (
278
- Code._0016,
279
- Code._1470,
280
- )
281
-
282
- OLD_CODES = dict.fromkeys(
283
- c for k in _DEV_KLASSES_HVAC.values() for c in k if c not in OUT_CODES
284
- )
285
- for code in OLD_CODES:
286
- gwy.send_cmd(_mk_cmd(RQ, code, "00", dev_id, qos=qos))
287
-
288
- NEW_CODES = (
289
- Code._0150,
290
- Code._042F,
291
- Code._1030,
292
- Code._10D0,
293
- Code._10E1,
294
- Code._2210,
295
- Code._22B0,
296
- Code._22E0,
297
- Code._22E5,
298
- Code._22E9,
299
- Code._22F1,
300
- Code._22F2,
301
- Code._22F3,
302
- Code._22F4,
303
- Code._22F7,
304
- Code._22F8,
305
- Code._2400,
306
- Code._2410,
307
- Code._2420,
308
- Code._313E,
309
- Code._3221,
310
- Code._3222,
311
- )
312
-
313
- for code in NEW_CODES:
314
- if code not in OLD_CODES and code not in OUT_CODES:
315
- gwy.send_cmd(_mk_cmd(RQ, code, "00", dev_id, qos=qos))
316
-
317
-
318
- @script_decorator
319
- async def script_scan_otb(gwy, dev_id: str):
320
- _LOGGER.warning("script_scan_otb_full invoked - expect a lot of nonsense")
321
-
322
- qos = {SZ_PRIORITY: Priority.LOW, SZ_RETRIES: 1}
323
- for msg_id in OTB_MSG_IDS:
324
- gwy.send_cmd(Command.get_opentherm_data(dev_id, msg_id, qos=qos))
325
-
326
-
327
- @script_decorator
328
- async def script_scan_otb_hard(gwy, dev_id: str):
329
- _LOGGER.warning("script_scan_otb_hard invoked - expect a lot of nonsense")
330
-
331
- for msg_id in range(0x80):
332
- gwy.send_cmd(Command.get_opentherm_data(dev_id, msg_id, qos=QOS_SCAN))
333
-
334
-
335
- @script_decorator
336
- async def script_scan_otb_map(gwy, dev_id: str): # Tested only upon a R8820A
337
- _LOGGER.warning("script_scan_otb_map invoked - expect a lot of nonsense")
338
-
339
- RAMSES_TO_OPENTHERM = {
340
- Code._22D9: "01", # boiler setpoint / ControlSetpoint
341
- Code._3EF1: "11", # rel. modulation level / RelativeModulationLevel
342
- Code._1300: "12", # cv water pressure / CHWaterPressure
343
- Code._12F0: "13", # dhw_flow_rate / DHWFlowRate
344
- Code._3200: "19", # boiler output temp / BoilerWaterTemperature
345
- Code._1260: "1A", # dhw temp / DHWTemperature
346
- Code._1290: "1B", # outdoor temp / OutsideTemperature
347
- Code._3210: "1C", # boiler return temp / ReturnWaterTemperature
348
- Code._10A0: "38", # dhw params[SZ_SETPOINT] / DHWSetpoint
349
- Code._1081: "39", # max ch setpoint / MaxCHWaterSetpoint
350
- }
351
-
352
- for code, msg_id in RAMSES_TO_OPENTHERM.items():
353
- gwy.send_cmd(_mk_cmd(RQ, code, "00", dev_id, qos=QOS_SCAN))
354
- gwy.send_cmd(Command.get_opentherm_data(dev_id, msg_id, qos=QOS_SCAN))
355
-
356
-
357
- @script_decorator
358
- async def script_scan_otb_ramses(gwy, dev_id: str): # Tested only upon a R8820A
359
- _LOGGER.warning("script_scan_otb_ramses invoked - expect a lot of nonsense")
360
-
361
- CODES = (
362
- Code._042F,
363
- Code._10E0, # device_info
364
- Code._10E1, # device_id
365
- Code._1FD0,
366
- Code._2400,
367
- Code._2401,
368
- Code._2410,
369
- Code._2420,
370
- Code._1300, # cv water pressure / CHWaterPressure
371
- Code._1081, # max ch setpoint / MaxCHWaterSetpoint
372
- Code._10A0, # dhw params[SZ_SETPOINT] / DHWSetpoint
373
- Code._22D9, # boiler setpoint / ControlSetpoint
374
- Code._1260, # dhw temp / DHWTemperature
375
- Code._1290, # outdoor temp / OutsideTemperature
376
- Code._3200, # boiler output temp / BoilerWaterTemperature
377
- Code._3210, # boiler return temp / ReturnWaterTemperature
378
- Code._0150,
379
- Code._12F0, # dhw flow rate / DHWFlowRate
380
- Code._1098,
381
- Code._10B0,
382
- Code._3221,
383
- Code._3223,
384
- Code._3EF0, # rel. modulation level / RelativeModulationLevel (also, below)
385
- Code._3EF1, # rel. modulation level / RelativeModulationLevel
386
- ) # excl. 3220
387
-
388
- # 3EF0 also includes:
389
- # - boiler status /
390
- # - ch setpoint /
391
- # - max. rel. modulation /
392
-
393
- [gwy.send_cmd(_mk_cmd(RQ, c, "00", dev_id, qos=QOS_SCAN)) for c in CODES]
394
-
395
-
396
- SCRIPTS = {
397
- k[7:]: v for k, v in locals().items() if callable(v) and k.startswith("script_")
398
- }
@@ -1,59 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- #
4
- """RAMSES RF - a RAMSES-II protocol decoder & analyser."""
5
- from __future__ import annotations
6
-
7
- from logging import Logger
8
-
9
- from .address import Address, is_valid_dev_id
10
- from .command import CODE_API_MAP, Command, FaultLog, Priority
11
- from .const import (
12
- SZ_DEVICE_ROLE,
13
- SZ_DOMAIN_ID,
14
- SZ_ZONE_CLASS,
15
- SZ_ZONE_IDX,
16
- SZ_ZONE_MASK,
17
- SZ_ZONE_TYPE,
18
- __dev_mode__,
19
- )
20
- from .exceptions import (
21
- CorruptStateError,
22
- ExpiredCallbackError,
23
- InvalidAddrSetError,
24
- InvalidPacketError,
25
- )
26
- from .logger import set_logger_timesource, set_pkt_logging
27
- from .message import Message
28
- from .packet import _PKT_LOGGER, Packet
29
- from .protocol import create_msg_stack
30
- from .ramses import CODES_BY_DEV_SLUG, CODES_SCHEMA
31
- from .schemas import SZ_SERIAL_PORT
32
- from .transport import SZ_POLLER_TASK, create_pkt_stack
33
-
34
- # noqa: F401, pylint: disable=unused-import
35
-
36
-
37
- # skipcq: PY-W2000
38
- from .const import ( # noqa: F401, isort: skip, pylint: disable=unused-import
39
- I_,
40
- RP,
41
- RQ,
42
- W_,
43
- F9,
44
- FA,
45
- FC,
46
- FF,
47
- DEV_ROLE,
48
- DEV_ROLE_MAP,
49
- DEV_TYPE,
50
- DEV_TYPE_MAP,
51
- ZON_ROLE,
52
- ZON_ROLE_MAP,
53
- Code,
54
- )
55
-
56
-
57
- def set_pkt_logging_config(**config) -> Logger:
58
- set_pkt_logging(_PKT_LOGGER, **config)
59
- return _PKT_LOGGER
@@ -1,42 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- #
4
- """RAMSES RF - a RAMSES-II protocol decoder & analyser.
5
-
6
- Enum backports from standard lib. Taken (with thanks) from
7
- https://github.com/home-assistant/core/homeassistant/backports
8
- """
9
- from __future__ import annotations
10
-
11
- from enum import Enum
12
- from typing import Any, TypeVar
13
-
14
- _StrEnumT = TypeVar("_StrEnumT", bound="StrEnum")
15
-
16
-
17
- class StrEnum(str, Enum):
18
- """Partial backport of Python 3.11's StrEnum for our basic use cases."""
19
-
20
- def __new__(
21
- cls: type[_StrEnumT], value: str, *args: Any, **kwargs: Any
22
- ) -> _StrEnumT:
23
- """Create a new StrEnum instance."""
24
- if not isinstance(value, str):
25
- raise TypeError(f"{value!r} is not a string")
26
- return super().__new__(cls, value, *args, **kwargs)
27
-
28
- def __str__(self) -> str:
29
- """Return self.value."""
30
- return str(self.value)
31
-
32
- @staticmethod
33
- def _generate_next_value_(
34
- name: str, start: int, count: int, last_values: list[Any]
35
- ) -> Any:
36
- """
37
- Make `auto()` explicitly unsupported.
38
-
39
- We may revisit this when it's very clear that Python 3.11's
40
- `StrEnum.auto()` behavior will no longer change.
41
- """
42
- raise TypeError("auto() is not supported by this implementation")