python-bsblan 1.2.2__tar.gz → 2.0.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: python-bsblan
3
- Version: 1.2.2
3
+ Version: 2.0.1
4
4
  Summary: Asynchronous Python client for BSBLAN API
5
5
  License: MIT
6
6
  Keywords: bsblan,thermostat,client,api,async
@@ -20,7 +20,6 @@ Classifier: Programming Language :: Python :: 3.12
20
20
  Classifier: Programming Language :: Python :: 3.13
21
21
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
22
  Requires-Dist: aiohttp (>=3.8.1)
23
- Requires-Dist: async-timeout (>=4.0.3,<5.0.0)
24
23
  Requires-Dist: backoff (>=2.2.1,<3.0.0)
25
24
  Requires-Dist: mashumaro (>=3.13.1,<4.0.0)
26
25
  Requires-Dist: orjson (>=3.9.10,<4.0.0)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "python-bsblan"
3
- version = "1.2.2"
3
+ version = "2.0.1"
4
4
  description = "Asynchronous Python client for BSBLAN API"
5
5
  authors = ["Willem-Jan van Rootselaar <liudgervr@gmail.com>"]
6
6
  maintainers = ["Willem-Jan van Rootselaar <liudgervr@gmail.com>"]
@@ -31,7 +31,6 @@ aiohttp = ">=3.8.1"
31
31
  yarl = ">=1.7.2"
32
32
  packaging = ">=21.3"
33
33
  backoff = "^2.2.1"
34
- async-timeout = "^4.0.3"
35
34
  mashumaro = "^3.13.1"
36
35
  orjson = "^3.9.10"
37
36
 
@@ -40,29 +39,29 @@ orjson = "^3.9.10"
40
39
  Changelog = "https://github.com/liudger/python-bsblan/releases"
41
40
 
42
41
  [tool.poetry.group.dev.dependencies]
43
- covdefaults = "^2.3.0"
44
- ruff = "^0.7.0"
45
- aresponses = "^3.0.0"
46
- black = "^25.0.0"
47
- blacken-docs = "^1.13.0"
48
- coverage = "^7.0.5"
49
- flake8 = "^7.0.0"
50
- isort = "^6.0.0"
51
- mypy = "^1.0.0"
52
- pre-commit = "^4.0.0"
53
- pre-commit-hooks = "^5.0.0"
54
- pylint = "^3.0.0"
55
- pytest = "^8.0.0"
56
- pytest-asyncio = "^0.25.0"
57
- pytest-cov = "^6.0.0"
58
- yamllint = "^1.29.0"
59
- pyupgrade = "^3.3.1"
60
- flake8-simplify = "^0.21.0"
61
- vulture = "^2.7"
62
- darglint = "^1.8.1"
63
- safety = "^3.0.0"
64
- codespell = "^2.2.2"
65
- bandit = "^1.7.4"
42
+ covdefaults = "2.3.0"
43
+ ruff = "0.7.4"
44
+ aresponses = "3.0.0"
45
+ black = "25.1.0"
46
+ blacken-docs = "1.19.1"
47
+ coverage = "7.8.2"
48
+ flake8 = "7.2.0"
49
+ isort = "6.0.1"
50
+ mypy = "1.16.0"
51
+ pre-commit = "4.2.0"
52
+ pre-commit-hooks = "5.0.0"
53
+ pylint = "3.3.7"
54
+ pytest = "8.3.5"
55
+ pytest-asyncio = "0.26.0"
56
+ pytest-cov = "6.1.1"
57
+ yamllint = "1.37.1"
58
+ pyupgrade = "3.19.1"
59
+ flake8-simplify = "0.22.0"
60
+ vulture = "2.14"
61
+ darglint = "1.8.1"
62
+ safety = "3.5.1"
63
+ codespell = "2.4.1"
64
+ bandit = "1.8.3"
66
65
 
67
66
 
68
67
  [tool.coverage.run]
@@ -2,13 +2,22 @@
2
2
 
3
3
  from .bsblan import BSBLAN, BSBLANConfig
4
4
  from .exceptions import BSBLANConnectionError, BSBLANError
5
- from .models import Device, HotWaterState, Info, Sensor, State, StaticState
5
+ from .models import (
6
+ Device,
7
+ DHWTimeSwitchPrograms,
8
+ HotWaterState,
9
+ Info,
10
+ Sensor,
11
+ State,
12
+ StaticState,
13
+ )
6
14
 
7
15
  __all__ = [
8
16
  "BSBLAN",
9
17
  "BSBLANConfig",
10
18
  "BSBLANConnectionError",
11
19
  "BSBLANError",
20
+ "DHWTimeSwitchPrograms",
12
21
  "Info",
13
22
  "State",
14
23
  "Device",
@@ -5,7 +5,10 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  import logging
7
7
  from dataclasses import dataclass, field
8
- from typing import TYPE_CHECKING, Any, Literal, Mapping, cast
8
+ from typing import TYPE_CHECKING, Any, Literal, cast
9
+
10
+ if TYPE_CHECKING:
11
+ from collections.abc import Mapping
9
12
 
10
13
  import aiohttp
11
14
  from aiohttp.hdrs import METH_POST
@@ -34,7 +37,15 @@ from .exceptions import (
34
37
  BSBLANInvalidParameterError,
35
38
  BSBLANVersionError,
36
39
  )
37
- from .models import Device, HotWaterState, Info, Sensor, State, StaticState
40
+ from .models import (
41
+ Device,
42
+ DHWTimeSwitchPrograms,
43
+ HotWaterState,
44
+ Info,
45
+ Sensor,
46
+ State,
47
+ StaticState,
48
+ )
38
49
  from .utility import APIValidator
39
50
 
40
51
  if TYPE_CHECKING:
@@ -200,6 +211,9 @@ class BSBLAN:
200
211
  version = pkg_version.parse(self._firmware_version)
201
212
  if version < pkg_version.parse("1.2.0"):
202
213
  self._api_version = "v1"
214
+ elif version >= pkg_version.parse("5.0.0"):
215
+ # BSB-LAN 5.0+ has breaking changes but uses v3-compatible API
216
+ self._api_version = "v3"
203
217
  elif version >= pkg_version.parse("3.0.0"):
204
218
  self._api_version = "v3"
205
219
  else:
@@ -298,7 +312,8 @@ class BSBLAN:
298
312
  headers=headers,
299
313
  ) as response:
300
314
  response.raise_for_status()
301
- return cast(dict[str, Any], await response.json())
315
+ response_data = cast("dict[str, Any]", await response.json())
316
+ return self._process_response(response_data, base_path)
302
317
  except asyncio.TimeoutError as e:
303
318
  raise BSBLANConnectionError(BSBLANConnectionError.message_timeout) from e
304
319
  except aiohttp.ClientError as e:
@@ -306,6 +321,38 @@ class BSBLAN:
306
321
  except ValueError as e:
307
322
  raise BSBLANError(str(e)) from e
308
323
 
324
+ def _process_response(
325
+ self, response_data: dict[str, Any], base_path: str
326
+ ) -> dict[str, Any]:
327
+ """Process response data based on firmware version.
328
+
329
+ BSB-LAN 5.0+ includes additional 'payload' field in /JQ responses
330
+ that needs to be handled for compatibility.
331
+
332
+ Args:
333
+ response_data: Raw response data from BSB-LAN
334
+ base_path: The API endpoint that was called
335
+
336
+ Returns:
337
+ Processed response data compatible with existing code
338
+
339
+ """
340
+ # For non-JQ endpoints, return response as-is
341
+ if base_path != "/JQ":
342
+ return response_data
343
+
344
+ # Check if we have a firmware version to determine processing
345
+ if not self._firmware_version:
346
+ return response_data
347
+
348
+ # For BSB-LAN 5.0+, remove 'payload' field if present as it's for debugging
349
+ version = pkg_version.parse(self._firmware_version)
350
+ if version >= pkg_version.parse("5.0.0") and "payload" in response_data:
351
+ # Remove payload field if present - it's added for debugging in 5.0+
352
+ return {k: v for k, v in response_data.items() if k != "payload"}
353
+
354
+ return response_data
355
+
309
356
  def _build_url(self, base_path: str) -> URL:
310
357
  """Build the URL for the request.
311
358
 
@@ -553,23 +600,75 @@ class BSBLAN:
553
600
  data = dict(zip(params["list"], list(data.values()), strict=True))
554
601
  return HotWaterState.from_dict(data)
555
602
 
556
- async def set_hot_water(
603
+ async def set_hot_water( # noqa: PLR0913
557
604
  self,
558
605
  nominal_setpoint: float | None = None,
559
606
  reduced_setpoint: float | None = None,
560
607
  operating_mode: str | None = None,
561
- ) -> None:
608
+ dhw_time_programs: DHWTimeSwitchPrograms | None = None,
609
+ eco_mode_selection: str | None = None,
610
+ dhw_charging_priority: str | None = None,
611
+ legionella_dwelling_time: float | None = None,
612
+ legionella_circulation_pump: str | None = None,
613
+ legionella_circulation_temp_diff: float | None = None,
614
+ dhw_circulation_pump_release: str | None = None,
615
+ dhw_circulation_pump_cycling: float | None = None,
616
+ dhw_circulation_setpoint: float | None = None,
617
+ operating_mode_changeover: str | None = None,
618
+ ) -> None: # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals
562
619
  """Change the state of the hot water system through BSB-Lan.
563
620
 
564
621
  Args:
565
622
  nominal_setpoint (float | None): The nominal setpoint temperature to set.
566
623
  reduced_setpoint (float | None): The reduced setpoint temperature to set.
624
+ operating_mode (str | None): The operating mode to set.
625
+ dhw_time_programs (DHWTimeSwitchPrograms | None): Time switch programs.
626
+ eco_mode_selection (str | None): Eco mode selection.
627
+ dhw_charging_priority (str | None): DHW charging priority.
628
+ legionella_dwelling_time (float | None): Legionella dwelling time.
629
+ legionella_circulation_pump (str | None): Legionella circulation pump.
630
+ legionella_circulation_temp_diff (float | None): Legionella circulation
631
+ temperature difference.
632
+ dhw_circulation_pump_release (str | None): DHW circulation pump release.
633
+ dhw_circulation_pump_cycling (float | None): DHW circulation pump cycling.
634
+ dhw_circulation_setpoint (float | None): DHW circulation setpoint.
635
+ operating_mode_changeover (str | None): Operating mode changeover.
567
636
 
568
637
  """
638
+ # Validate only one parameter is being set
639
+ time_program_params = []
640
+ if dhw_time_programs:
641
+ if dhw_time_programs.monday:
642
+ time_program_params.append(dhw_time_programs.monday)
643
+ if dhw_time_programs.tuesday:
644
+ time_program_params.append(dhw_time_programs.tuesday)
645
+ if dhw_time_programs.wednesday:
646
+ time_program_params.append(dhw_time_programs.wednesday)
647
+ if dhw_time_programs.thursday:
648
+ time_program_params.append(dhw_time_programs.thursday)
649
+ if dhw_time_programs.friday:
650
+ time_program_params.append(dhw_time_programs.friday)
651
+ if dhw_time_programs.saturday:
652
+ time_program_params.append(dhw_time_programs.saturday)
653
+ if dhw_time_programs.sunday:
654
+ time_program_params.append(dhw_time_programs.sunday)
655
+ if dhw_time_programs.standard_values:
656
+ time_program_params.append(dhw_time_programs.standard_values)
657
+
569
658
  self._validate_single_parameter(
570
659
  nominal_setpoint,
571
660
  reduced_setpoint,
572
661
  operating_mode,
662
+ eco_mode_selection,
663
+ dhw_charging_priority,
664
+ legionella_dwelling_time,
665
+ legionella_circulation_pump,
666
+ legionella_circulation_temp_diff,
667
+ dhw_circulation_pump_release,
668
+ dhw_circulation_pump_cycling,
669
+ dhw_circulation_setpoint,
670
+ operating_mode_changeover,
671
+ *time_program_params,
573
672
  error_msg=MULTI_PARAMETER_ERROR_MSG,
574
673
  )
575
674
 
@@ -577,20 +676,52 @@ class BSBLAN:
577
676
  nominal_setpoint,
578
677
  reduced_setpoint,
579
678
  operating_mode,
679
+ dhw_time_programs,
680
+ eco_mode_selection,
681
+ dhw_charging_priority,
682
+ legionella_dwelling_time,
683
+ legionella_circulation_pump,
684
+ legionella_circulation_temp_diff,
685
+ dhw_circulation_pump_release,
686
+ dhw_circulation_pump_cycling,
687
+ dhw_circulation_setpoint,
688
+ operating_mode_changeover,
580
689
  )
581
690
  await self._set_hot_water_state(state)
582
691
 
583
- def _prepare_hot_water_state(
692
+ def _prepare_hot_water_state( # noqa: PLR0913, PLR0912
584
693
  self,
585
694
  nominal_setpoint: float | None,
586
695
  reduced_setpoint: float | None,
587
696
  operating_mode: str | None,
588
- ) -> dict[str, Any]:
697
+ dhw_time_programs: DHWTimeSwitchPrograms | None = None,
698
+ eco_mode_selection: str | None = None,
699
+ dhw_charging_priority: str | None = None,
700
+ legionella_dwelling_time: float | None = None,
701
+ legionella_circulation_pump: str | None = None,
702
+ legionella_circulation_temp_diff: float | None = None,
703
+ dhw_circulation_pump_release: str | None = None,
704
+ dhw_circulation_pump_cycling: float | None = None,
705
+ dhw_circulation_setpoint: float | None = None,
706
+ operating_mode_changeover: str | None = None,
707
+ ) -> dict[str, Any]: # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals,too-many-branches
589
708
  """Prepare the hot water state for setting.
590
709
 
591
710
  Args:
592
711
  nominal_setpoint (float | None): The nominal setpoint temperature to set.
593
712
  reduced_setpoint (float | None): The reduced setpoint temperature to set.
713
+ operating_mode (str | None): The operating mode to set.
714
+ dhw_time_programs (DHWTimeSwitchPrograms | None): Time switch programs.
715
+ eco_mode_selection (str | None): Eco mode selection.
716
+ dhw_charging_priority (str | None): DHW charging priority.
717
+ legionella_dwelling_time (float | None): Legionella dwelling time.
718
+ legionella_circulation_pump (str | None): Legionella circulation pump.
719
+ legionella_circulation_temp_diff (float | None): Legionella circulation
720
+ temperature difference.
721
+ dhw_circulation_pump_release (str | None): DHW circulation pump release.
722
+ dhw_circulation_pump_cycling (float | None): DHW circulation pump cycling.
723
+ dhw_circulation_setpoint (float | None): DHW circulation setpoint.
724
+ operating_mode_changeover (str | None): Operating mode changeover.
594
725
 
595
726
  Returns:
596
727
  dict[str, Any]: The prepared state for the hot water.
@@ -616,6 +747,95 @@ class BSBLAN:
616
747
  "Type": "1",
617
748
  },
618
749
  )
750
+ if eco_mode_selection is not None:
751
+ state.update(
752
+ {
753
+ "Parameter": "1601",
754
+ "EnumValue": eco_mode_selection,
755
+ "Type": "1",
756
+ },
757
+ )
758
+ if dhw_charging_priority is not None:
759
+ state.update(
760
+ {
761
+ "Parameter": "1630",
762
+ "EnumValue": dhw_charging_priority,
763
+ "Type": "1",
764
+ },
765
+ )
766
+ if legionella_dwelling_time is not None:
767
+ state.update(
768
+ {
769
+ "Parameter": "1646",
770
+ "Value": str(legionella_dwelling_time),
771
+ "Type": "1",
772
+ },
773
+ )
774
+ if legionella_circulation_pump is not None:
775
+ state.update(
776
+ {
777
+ "Parameter": "1647",
778
+ "EnumValue": legionella_circulation_pump,
779
+ "Type": "1",
780
+ },
781
+ )
782
+ if legionella_circulation_temp_diff is not None:
783
+ state.update(
784
+ {
785
+ "Parameter": "1648",
786
+ "Value": str(legionella_circulation_temp_diff),
787
+ "Type": "1",
788
+ },
789
+ )
790
+ if dhw_circulation_pump_release is not None:
791
+ state.update(
792
+ {
793
+ "Parameter": "1660",
794
+ "EnumValue": dhw_circulation_pump_release,
795
+ "Type": "1",
796
+ },
797
+ )
798
+ if dhw_circulation_pump_cycling is not None:
799
+ state.update(
800
+ {
801
+ "Parameter": "1661",
802
+ "Value": str(dhw_circulation_pump_cycling),
803
+ "Type": "1",
804
+ },
805
+ )
806
+ if dhw_circulation_setpoint is not None:
807
+ state.update(
808
+ {
809
+ "Parameter": "1663",
810
+ "Value": str(dhw_circulation_setpoint),
811
+ "Type": "1",
812
+ },
813
+ )
814
+ if operating_mode_changeover is not None:
815
+ state.update(
816
+ {
817
+ "Parameter": "1680",
818
+ "EnumValue": operating_mode_changeover,
819
+ "Type": "1",
820
+ },
821
+ )
822
+
823
+ if dhw_time_programs:
824
+ time_program_mapping = {
825
+ "561": dhw_time_programs.monday,
826
+ "562": dhw_time_programs.tuesday,
827
+ "563": dhw_time_programs.wednesday,
828
+ "564": dhw_time_programs.thursday,
829
+ "565": dhw_time_programs.friday,
830
+ "566": dhw_time_programs.saturday,
831
+ "567": dhw_time_programs.sunday,
832
+ "576": dhw_time_programs.standard_values,
833
+ }
834
+
835
+ for param, value in time_program_mapping.items():
836
+ if value is not None:
837
+ state.update({"Parameter": param, "Value": value, "Type": "1"})
838
+
619
839
  if not state:
620
840
  raise BSBLANError(NO_STATE_ERROR_MSG)
621
841
  return state
@@ -51,17 +51,34 @@ API_V1: Final[APIConfig] = {
51
51
  },
52
52
  "hot_water": {
53
53
  "1600": "operating_mode",
54
+ "1601": "eco_mode_selection",
54
55
  "1610": "nominal_setpoint",
55
56
  "1614": "nominal_setpoint_max",
56
57
  "1612": "reduced_setpoint",
57
58
  "1620": "release",
59
+ "1630": "dhw_charging_priority",
58
60
  "1640": "legionella_function",
59
61
  "1645": "legionella_setpoint",
60
62
  "1641": "legionella_periodicity",
61
63
  "1642": "legionella_function_day",
62
64
  "1643": "legionella_function_time",
65
+ "1646": "legionella_dwelling_time",
66
+ "1647": "legionella_circulation_pump",
67
+ "1648": "legionella_circulation_temp_diff",
68
+ "1660": "dhw_circulation_pump_release",
69
+ "1661": "dhw_circulation_pump_cycling",
70
+ "1663": "dhw_circulation_setpoint",
71
+ "1680": "operating_mode_changeover",
63
72
  "8830": "dhw_actual_value_top_temperature",
64
73
  "8820": "state_dhw_pump",
74
+ "561": "dhw_time_program_monday",
75
+ "562": "dhw_time_program_tuesday",
76
+ "563": "dhw_time_program_wednesday",
77
+ "564": "dhw_time_program_thursday",
78
+ "565": "dhw_time_program_friday",
79
+ "566": "dhw_time_program_saturday",
80
+ "567": "dhw_time_program_sunday",
81
+ "576": "dhw_time_program_standard_values",
65
82
  },
66
83
  }
67
84
 
@@ -90,17 +107,34 @@ API_V3: Final[APIConfig] = {
90
107
  },
91
108
  "hot_water": {
92
109
  "1600": "operating_mode",
110
+ "1601": "eco_mode_selection",
93
111
  "1610": "nominal_setpoint",
94
112
  "1614": "nominal_setpoint_max",
95
113
  "1612": "reduced_setpoint",
96
114
  "1620": "release",
115
+ "1630": "dhw_charging_priority",
97
116
  "1640": "legionella_function",
98
117
  "1645": "legionella_setpoint",
99
118
  "1641": "legionella_periodicity",
100
119
  "1642": "legionella_function_day",
101
120
  "1644": "legionella_function_time",
121
+ "1646": "legionella_dwelling_time",
122
+ "1647": "legionella_circulation_pump",
123
+ "1648": "legionella_circulation_temp_diff",
124
+ "1660": "dhw_circulation_pump_release",
125
+ "1661": "dhw_circulation_pump_cycling",
126
+ "1663": "dhw_circulation_setpoint",
127
+ "1680": "operating_mode_changeover",
102
128
  "8830": "dhw_actual_value_top_temperature",
103
129
  "8820": "state_dhw_pump",
130
+ "561": "dhw_time_program_monday",
131
+ "562": "dhw_time_program_tuesday",
132
+ "563": "dhw_time_program_wednesday",
133
+ "564": "dhw_time_program_thursday",
134
+ "565": "dhw_time_program_friday",
135
+ "566": "dhw_time_program_saturday",
136
+ "567": "dhw_time_program_sunday",
137
+ "576": "dhw_time_program_standard_values",
104
138
  },
105
139
  }
106
140
 
@@ -14,6 +14,20 @@ from mashumaro.mixins.json import DataClassJSONMixin
14
14
  from bsblan.constants import TEMPERATURE_UNITS
15
15
 
16
16
 
17
+ @dataclass
18
+ class DHWTimeSwitchPrograms:
19
+ """Dataclass for DHW time switch programs."""
20
+
21
+ monday: str | None = None
22
+ tuesday: str | None = None
23
+ wednesday: str | None = None
24
+ thursday: str | None = None
25
+ friday: str | None = None
26
+ saturday: str | None = None
27
+ sunday: str | None = None
28
+ standard_values: str | None = None
29
+
30
+
17
31
  class DataType(IntEnum):
18
32
  """Enumeration of BSB-LAN data types."""
19
33
 
@@ -163,21 +177,38 @@ class Sensor(DataClassJSONMixin):
163
177
 
164
178
 
165
179
  @dataclass
166
- class HotWaterState(DataClassJSONMixin):
180
+ class HotWaterState(DataClassJSONMixin): # pylint: disable=too-many-instance-attributes
167
181
  """Object holds info about object for hot water climate."""
168
182
 
169
183
  operating_mode: EntityInfo | None = None
184
+ eco_mode_selection: EntityInfo | None = None
170
185
  nominal_setpoint: EntityInfo | None = None
171
186
  nominal_setpoint_max: EntityInfo | None = None
172
187
  reduced_setpoint: EntityInfo | None = None
173
188
  release: EntityInfo | None = None
189
+ dhw_charging_priority: EntityInfo | None = None
174
190
  legionella_function: EntityInfo | None = None
175
191
  legionella_setpoint: EntityInfo | None = None
176
192
  legionella_periodicity: EntityInfo | None = None
177
193
  legionella_function_day: EntityInfo | None = None
178
194
  legionella_function_time: EntityInfo | None = None
195
+ legionella_dwelling_time: EntityInfo | None = None
196
+ legionella_circulation_pump: EntityInfo | None = None
197
+ legionella_circulation_temp_diff: EntityInfo | None = None
198
+ dhw_circulation_pump_release: EntityInfo | None = None
199
+ dhw_circulation_pump_cycling: EntityInfo | None = None
200
+ dhw_circulation_setpoint: EntityInfo | None = None
201
+ operating_mode_changeover: EntityInfo | None = None
179
202
  dhw_actual_value_top_temperature: EntityInfo | None = None
180
203
  state_dhw_pump: EntityInfo | None = None
204
+ dhw_time_program_monday: EntityInfo | None = None
205
+ dhw_time_program_tuesday: EntityInfo | None = None
206
+ dhw_time_program_wednesday: EntityInfo | None = None
207
+ dhw_time_program_thursday: EntityInfo | None = None
208
+ dhw_time_program_friday: EntityInfo | None = None
209
+ dhw_time_program_saturday: EntityInfo | None = None
210
+ dhw_time_program_sunday: EntityInfo | None = None
211
+ dhw_time_program_standard_values: EntityInfo | None = None
181
212
 
182
213
 
183
214
  @dataclass
File without changes