python-bsblan 5.2.0__py3-none-any.whl → 6.0.0__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.
bsblan/bsblan.py CHANGED
@@ -19,6 +19,8 @@ from yarl import URL
19
19
 
20
20
  from .constants import (
21
21
  API_VERSIONS,
22
+ PPS_HEATING_PARAMS,
23
+ PPS_STATIC_VALUES_PARAMS,
22
24
  APIConfig,
23
25
  CircuitConfig,
24
26
  ErrorMsg,
@@ -100,6 +102,7 @@ class BSBLAN:
100
102
  _firmware_version: str | None = None
101
103
  _api_version: str | None = None
102
104
  _api_data: APIConfig | None = None
105
+ _device: Device | None = None
103
106
  _initialized: bool = False
104
107
  _api_validator: APIValidator = field(init=False)
105
108
  _temperature_unit: str = "°C"
@@ -152,13 +155,10 @@ class BSBLAN:
152
155
  async def get_available_circuits(self) -> list[int]:
153
156
  """Detect which heating circuits are available on the device.
154
157
 
155
- Uses a two-step probe for each circuit (1, 2):
156
- 1. Query the operating mode parameter — the response must be
157
- non-empty and contain actual data.
158
- 2. Query the status parameter (8000/8001) an inactive
159
- circuit returns ``value="0"`` with ``desc="---"``.
160
-
161
- A circuit is only considered available when both checks pass.
158
+ Uses the configured operating mode probe parameters from
159
+ CircuitConfig.PROBE_PARAMS as the only discovery signal. Status
160
+ parameters are not queried during discovery to keep setup lightweight
161
+ and avoid excluding valid circuits when status data is unavailable.
162
162
 
163
163
  This is useful for integration setup flows (e.g., Home Assistant
164
164
  config flow) to discover how many circuits the user's controller
@@ -173,54 +173,48 @@ class BSBLAN:
173
173
  # circuits == [1, 2] for a dual-circuit controller
174
174
 
175
175
  """
176
+ if self._uses_pps_bus:
177
+ return await self._get_available_pps_circuits()
178
+
176
179
  available: list[int] = []
177
180
  for circuit, param_id in CircuitConfig.PROBE_PARAMS.items():
178
181
  try:
179
182
  response = await self._request(
180
183
  params={"Parameter": param_id},
181
184
  )
182
- # A circuit exists if the response contains the param_id key
183
- # with actual data (not an empty dict)
184
- if not response.get(param_id):
185
- continue
186
-
187
- # Secondary check: query the status parameter.
188
- # Inactive circuits either:
189
- # - return value="0" and desc="---"
190
- # - return an empty dict {} (param not supported)
191
- status_id = CircuitConfig.STATUS_PARAMS[circuit]
192
- status_resp = await self._request(
193
- params={"Parameter": status_id},
185
+ except BSBLANError:
186
+ logger.debug(
187
+ "Circuit %d not available (operating mode request failed)",
188
+ circuit,
194
189
  )
195
- status_data = status_resp.get(status_id, {})
190
+ continue
196
191
 
197
- # Empty response means the parameter doesn't exist
198
- if not status_data or not isinstance(status_data, dict):
199
- logger.debug(
200
- "Circuit %d has no status data (not supported)",
201
- circuit,
202
- )
203
- continue
204
-
205
- # value="0" + desc="---" means inactive
206
- if (
207
- status_data.get("desc") == CircuitConfig.INACTIVE_MARKER
208
- and str(status_data.get("value", "")) == "0"
209
- ):
210
- logger.debug(
211
- "Circuit %d has status '---' (inactive)",
212
- circuit,
213
- )
214
- continue
215
-
216
- available.append(circuit)
217
- except BSBLANError:
192
+ # A circuit exists if the response contains the operating mode key
193
+ # with actual data (not an empty dict).
194
+ if not response.get(param_id):
218
195
  logger.debug(
219
- "Circuit %d not available (request failed)",
196
+ "Circuit %d has no operating mode data (not supported)",
220
197
  circuit,
221
198
  )
199
+ continue
200
+
201
+ available.append(circuit)
222
202
  return sorted(available)
223
203
 
204
+ async def _get_available_pps_circuits(self) -> list[int]:
205
+ """Detect the single PPS room-unit climate circuit."""
206
+ param_id = "15000"
207
+ try:
208
+ response = await self._request(params={"Parameter": param_id})
209
+ except BSBLANError:
210
+ logger.debug("PPS climate circuit not available")
211
+ return []
212
+
213
+ if not response.get(param_id):
214
+ logger.debug("PPS climate circuit has no operating mode data")
215
+ return []
216
+ return [1]
217
+
224
218
  async def _setup_api_validator(self) -> None:
225
219
  """Set up the API validator without validating sections.
226
220
 
@@ -234,9 +228,21 @@ class BSBLAN:
234
228
  if self._api_data is None:
235
229
  self._api_data = self._copy_api_config()
236
230
 
231
+ self._apply_bus_specific_api_config()
232
+
237
233
  # Initialize the API validator (but don't validate sections yet)
238
234
  self._api_validator = APIValidator(self._api_data)
239
235
 
236
+ def _apply_bus_specific_api_config(self) -> None:
237
+ """Apply bus-specific parameter maps to the current API config."""
238
+ if self._api_data is None or not self._uses_pps_bus:
239
+ return
240
+
241
+ self._api_data["heating"] = PPS_HEATING_PARAMS.copy()
242
+ self._api_data["staticValues"] = PPS_STATIC_VALUES_PARAMS.copy()
243
+ self._api_data["heating_circuit2"] = {}
244
+ self._api_data["staticValues_circuit2"] = {}
245
+
240
246
  async def _ensure_section_validated(
241
247
  self, section: SectionLiteral, include: list[str] | None = None
242
248
  ) -> None:
@@ -273,11 +279,26 @@ class BSBLAN:
273
279
  logger.debug("Lazy loading section: %s", section)
274
280
  response_data = await self._validate_api_section(section, include)
275
281
 
276
- # Extract temperature unit from heating section validation
277
- # (parameter 710 - target_temperature is always in heating section)
278
- if section == "heating" and response_data:
282
+ if response_data and self._should_extract_temperature_unit(
283
+ section, include, response_data
284
+ ):
279
285
  self._extract_temperature_unit_from_response(response_data)
280
286
 
287
+ def _should_extract_temperature_unit(
288
+ self,
289
+ section: SectionLiteral,
290
+ include: list[str] | None,
291
+ response_data: dict[str, Any],
292
+ ) -> bool:
293
+ """Return whether the validation response should update temperature unit."""
294
+ if section != "heating":
295
+ return False
296
+
297
+ if include is None or "target_temperature" in include:
298
+ return True
299
+
300
+ return any(param_id in response_data for param_id in ("710", "15004"))
301
+
281
302
  async def _ensure_hot_water_group_validated(
282
303
  self,
283
304
  group_name: str,
@@ -470,17 +491,16 @@ class BSBLAN:
470
491
  ) -> None:
471
492
  """Extract temperature unit from heating section response data.
472
493
 
473
- Gets the unit from parameter 710 (target_temperature) which is always
494
+ Gets the unit from the target_temperature parameter, which is always
474
495
  present in the heating section.
475
496
 
476
497
  Args:
477
498
  response_data: The response data from heating section validation
478
499
 
479
500
  """
480
- # Look for parameter 710 (target_temperature) in the response
501
+ # Look for target_temperature in the response.
481
502
  for param_id, param_data in response_data.items():
482
- # Check if this is parameter 710 and has unit information
483
- if param_id == "710" and isinstance(param_data, dict):
503
+ if param_id in {"710", "15004"} and isinstance(param_data, dict):
484
504
  unit = param_data.get("unit", "")
485
505
  if unit in ("°C", "°C"):
486
506
  self._temperature_unit = "°C"
@@ -489,16 +509,15 @@ class BSBLAN:
489
509
  else:
490
510
  # Keep default if unit is empty or unknown
491
511
  logger.debug(
492
- "Unknown or empty temperature unit from parameter 710: '%s'. "
493
- "Using default (°C)",
512
+ "Unknown or empty temperature unit from heating target: "
513
+ "'%s'. Using default (°C)",
494
514
  unit,
495
515
  )
496
516
  logger.debug("Temperature unit set to: %s", self._temperature_unit)
497
517
  return
498
518
 
499
- # If we didn't find parameter 710, log a warning
500
519
  logger.warning(
501
- "Could not find parameter 710 in heating section response. "
520
+ "Could not find target temperature in heating section response. "
502
521
  "Using default temperature unit (°C)"
503
522
  )
504
523
 
@@ -520,6 +539,31 @@ class BSBLAN:
520
539
  logger.debug("BSBLAN version: %s", self._firmware_version)
521
540
  self._set_api_version()
522
541
 
542
+ @property
543
+ def device_info(self) -> Device | None:
544
+ """Return cached device metadata from the last /JI response."""
545
+ return self._device
546
+
547
+ @property
548
+ def supports_time_sync(self) -> bool:
549
+ """Return cached support for the normal BSB/LPB time sync command."""
550
+ return self._device is not None and self._device.supports_time_sync
551
+
552
+ @property
553
+ def _uses_pps_bus(self) -> bool:
554
+ """Return whether cached metadata identifies the device as PPS."""
555
+ return self._device is not None and self._device.is_pps_bus
556
+
557
+ @property
558
+ def _is_bus_writable(self) -> bool:
559
+ """Return whether cached metadata says writes are allowed."""
560
+ return self._device is None or self._device.is_bus_writable
561
+
562
+ async def _ensure_device_metadata(self) -> None:
563
+ """Fetch device metadata if it has not been loaded yet."""
564
+ if self._device is None:
565
+ await self.device()
566
+
523
567
  def _set_api_version(self) -> None:
524
568
  """Set the API version based on the firmware version.
525
569
 
@@ -598,8 +642,8 @@ class BSBLAN:
598
642
  Args:
599
643
  circuit: The heating circuit number (1 or 2).
600
644
 
601
- Note: Temperature unit is extracted during heating section validation
602
- from the response (parameter 710), so no extra API call is needed here.
645
+ Note: Temperature unit is extracted during heating section validation,
646
+ so no extra API call is needed here.
603
647
 
604
648
  """
605
649
  if circuit in self._circuit_temp_initialized:
@@ -619,10 +663,20 @@ class BSBLAN:
619
663
  BSBLANInvalidParameterError: If the circuit number is invalid.
620
664
 
621
665
  """
622
- if circuit not in CircuitConfig.VALID:
666
+ if circuit not in CircuitConfig.VALID or (self._uses_pps_bus and circuit != 1):
623
667
  msg = ErrorMsg.INVALID_CIRCUIT.format(circuit)
624
668
  raise BSBLANInvalidParameterError(msg)
625
669
 
670
+ def _validate_bus_write_supported(self) -> None:
671
+ """Validate that cached metadata permits writes."""
672
+ if not self._is_bus_writable:
673
+ raise BSBLANError(ErrorMsg.BUS_WRITE_NOT_SUPPORTED)
674
+
675
+ def _validate_time_sync_supported(self) -> None:
676
+ """Validate that normal parameter 0 time sync is safe."""
677
+ if not self.supports_time_sync:
678
+ raise BSBLANError(ErrorMsg.TIME_SYNC_NOT_SUPPORTED)
679
+
626
680
  @property
627
681
  def get_temperature_unit(self) -> str:
628
682
  """Get the unit of temperature.
@@ -904,8 +958,26 @@ class BSBLAN:
904
958
  params = self._extract_params_summary(section_params)
905
959
  data = await self._request(params={"Parameter": params["string_par"]})
906
960
  data = dict(zip(params["list"], list(data.values()), strict=True))
961
+ if section == "heating" and self._uses_pps_bus:
962
+ self._normalize_pps_state_data(data)
907
963
  return model_class.model_validate(data)
908
964
 
965
+ def _normalize_pps_state_data(self, data: dict[str, Any]) -> None:
966
+ """Normalize PPS climate values to the library's State model."""
967
+ hvac_mode = data.get("hvac_mode")
968
+ if not isinstance(hvac_mode, dict):
969
+ return
970
+
971
+ try:
972
+ raw_mode = int(hvac_mode["value"])
973
+ except (KeyError, TypeError, ValueError):
974
+ return
975
+
976
+ hvac_mode["value"] = Validation.PPS_HVAC_MODE_FROM_BSBLAN.get(
977
+ raw_mode,
978
+ raw_mode,
979
+ )
980
+
909
981
  async def state(
910
982
  self,
911
983
  include: list[str] | None = None,
@@ -927,8 +999,9 @@ class BSBLAN:
927
999
  State: The current state of the BSBLAN device.
928
1000
 
929
1001
  Note:
930
- The hvac_mode.value is returned as a raw integer from the device:
931
- 0=off, 1=auto, 2=eco, 3=heat.
1002
+ For BSB/LPB devices, hvac_mode.value is returned as a raw integer:
1003
+ 0=off, 1=auto, 2=eco, 3=heat. PPS devices normalize their raw
1004
+ operating modes to the same library values, but do not support eco.
932
1005
 
933
1006
  Example:
934
1007
  # Fetch only hvac_mode and current_temperature
@@ -1000,7 +1073,8 @@ class BSBLAN:
1000
1073
 
1001
1074
  """
1002
1075
  device_info = await self._request(base_path="/JI")
1003
- return Device.model_validate(device_info)
1076
+ self._device = Device.model_validate(device_info)
1077
+ return self._device
1004
1078
 
1005
1079
  async def info(self, include: list[str] | None = None) -> Info:
1006
1080
  """Get information about the current heating system config.
@@ -1027,6 +1101,9 @@ class BSBLAN:
1027
1101
  DeviceTime: The current time information from the BSB-LAN device.
1028
1102
 
1029
1103
  """
1104
+ await self._ensure_device_metadata()
1105
+ self._validate_time_sync_supported()
1106
+
1030
1107
  # Get only parameter 0 for time
1031
1108
  data = await self._request(params={"Parameter": "0"})
1032
1109
  # Create the data dictionary in the expected format
@@ -1044,6 +1121,8 @@ class BSBLAN:
1044
1121
  BSBLANInvalidParameterError: If the time format is invalid.
1045
1122
 
1046
1123
  """
1124
+ await self._ensure_device_metadata()
1125
+ self._validate_time_sync_supported()
1047
1126
  self._validate_time_format(time_value)
1048
1127
  state: dict[str, object] = {
1049
1128
  "Parameter": "0",
@@ -1064,7 +1143,9 @@ class BSBLAN:
1064
1143
  Args:
1065
1144
  target_temperature (str | None): The target temperature to set.
1066
1145
  hvac_mode (int | None): The HVAC mode to set as raw integer value.
1067
- Valid values: 0=off, 1=auto, 2=eco, 3=heat.
1146
+ For BSB/LPB, valid values are 0=off, 1=auto, 2=eco, 3=heat.
1147
+ For PPS, valid values are 0=off, 1=auto, and 3=heat/manual;
1148
+ they are translated to PPS raw values before posting.
1068
1149
  circuit: The heating circuit number (1 or 2). Defaults to 1.
1069
1150
 
1070
1151
  Example:
@@ -1076,6 +1157,8 @@ class BSBLAN:
1076
1157
 
1077
1158
  """
1078
1159
  self._validate_circuit(circuit)
1160
+ if self._uses_pps_bus:
1161
+ self._validate_bus_write_supported()
1079
1162
  await self._initialize_temperature_range(circuit)
1080
1163
 
1081
1164
  self._validate_single_parameter(
@@ -1108,7 +1191,7 @@ class BSBLAN:
1108
1191
  dict[str, Any]: The prepared state for the thermostat.
1109
1192
 
1110
1193
  """
1111
- param_ids = CircuitConfig.THERMOSTAT_PARAMS[circuit]
1194
+ param_ids = self._thermostat_params(circuit)
1112
1195
  state: dict[str, Any] = {}
1113
1196
  if target_temperature is not None:
1114
1197
  await self._validate_target_temperature(
@@ -1124,15 +1207,24 @@ class BSBLAN:
1124
1207
  )
1125
1208
  if hvac_mode is not None:
1126
1209
  self._validate_hvac_mode(hvac_mode)
1210
+ hvac_value = str(hvac_mode)
1211
+ if self._uses_pps_bus:
1212
+ hvac_value = Validation.PPS_HVAC_MODE_TO_BSBLAN[hvac_mode]
1127
1213
  state.update(
1128
1214
  {
1129
1215
  "Parameter": param_ids["hvac_mode"],
1130
- "Value": str(hvac_mode),
1216
+ "Value": hvac_value,
1131
1217
  "Type": "1",
1132
1218
  },
1133
1219
  )
1134
1220
  return state
1135
1221
 
1222
+ def _thermostat_params(self, circuit: int) -> dict[str, str]:
1223
+ """Return thermostat write parameters for the active bus type."""
1224
+ if self._uses_pps_bus:
1225
+ return {"target_temperature": "15004", "hvac_mode": "15000"}
1226
+ return CircuitConfig.THERMOSTAT_PARAMS[circuit]
1227
+
1136
1228
  async def _validate_target_temperature(
1137
1229
  self,
1138
1230
  target_temperature: str,
@@ -1177,13 +1269,17 @@ class BSBLAN:
1177
1269
  """Validate the HVAC mode.
1178
1270
 
1179
1271
  Args:
1180
- hvac_mode (int): The HVAC mode to validate (0-3).
1272
+ hvac_mode (int): The HVAC mode to validate. BSB/LPB accepts 0-3;
1273
+ PPS accepts 0, 1, and 3.
1181
1274
 
1182
1275
  Raises:
1183
1276
  BSBLANInvalidParameterError: If the HVAC mode is invalid.
1184
1277
 
1185
1278
  """
1186
- if hvac_mode not in Validation.HVAC_MODES:
1279
+ valid_modes = (
1280
+ Validation.PPS_HVAC_MODES if self._uses_pps_bus else Validation.HVAC_MODES
1281
+ )
1282
+ if hvac_mode not in valid_modes:
1187
1283
  raise BSBLANInvalidParameterError(str(hvac_mode))
1188
1284
 
1189
1285
  def _validate_time_format(self, time_value: str) -> None:
bsblan/constants.py CHANGED
@@ -114,6 +114,19 @@ BASE_STATIC_VALUES_CIRCUIT2_PARAMS: Final[dict[str, str]] = {
114
114
  "1014": "min_temp",
115
115
  }
116
116
 
117
+ # PPS bus supports one room-unit style climate circuit. These parameters are
118
+ # exposed by BSB-LAN in the 15000+ range and mirror the circuit 1 climate model.
119
+ PPS_HEATING_PARAMS: Final[dict[str, str]] = {
120
+ "15000": "hvac_mode",
121
+ "15004": "target_temperature",
122
+ "15008": "current_temperature",
123
+ }
124
+
125
+ PPS_STATIC_VALUES_PARAMS: Final[dict[str, str]] = {
126
+ "15006": "min_temp",
127
+ "15007": "max_temp",
128
+ }
129
+
117
130
  V1_STATIC_VALUES_CIRCUIT2_EXTENSIONS: Final[dict[str, str]] = {
118
131
  "1030": "max_temp",
119
132
  }
@@ -207,6 +220,17 @@ class Validation:
207
220
  """Validation-related constants for BSBLAN."""
208
221
 
209
222
  HVAC_MODES: Final[set[int]] = {0, 1, 2, 3}
223
+ PPS_HVAC_MODES: Final[set[int]] = {0, 1, 3}
224
+ PPS_HVAC_MODE_TO_BSBLAN: Final[dict[int, str]] = {
225
+ 0: "2", # off
226
+ 1: "0", # auto
227
+ 3: "1", # manual/comfort
228
+ }
229
+ PPS_HVAC_MODE_FROM_BSBLAN: Final[dict[int, int]] = {
230
+ 0: 1, # auto
231
+ 1: 3, # manual/comfort
232
+ 2: 0, # off
233
+ }
210
234
  MIN_YEAR: Final[int] = 1900
211
235
  MAX_YEAR: Final[int] = 2100
212
236
 
@@ -492,6 +516,8 @@ class ErrorMsg:
492
516
  "Empty include list provided. Use None to fetch all parameters."
493
517
  )
494
518
  NO_HEATING_SCHEDULE_PARAMS = "No heating schedule parameters available"
519
+ TIME_SYNC_NOT_SUPPORTED = "Time synchronization is not supported by this device"
520
+ BUS_WRITE_NOT_SUPPORTED = "Writing parameters is not supported by this device"
495
521
 
496
522
 
497
523
  # Handle both ASCII and Unicode degree symbols
bsblan/models.py CHANGED
@@ -326,13 +326,13 @@ class EntityInfo(BaseModel, Generic[T]):
326
326
  unit: str
327
327
  desc: str
328
328
  value: T | None = None
329
- data_type: int = Field(alias="dataType", default=0)
329
+ data_type: int = Field(validation_alias="dataType", default=0)
330
330
  error: int = 0
331
331
  readonly: int = 0
332
332
  readwrite: int = 0
333
333
  precision: float | None = None
334
- data_type_name: str = Field(default="", alias="dataType_name")
335
- data_type_family: str = Field(default="", alias="dataType_family")
334
+ data_type_name: str = Field(default="", validation_alias="dataType_name")
335
+ data_type_family: str = Field(default="", validation_alias="dataType_family")
336
336
 
337
337
  @model_validator(mode="before")
338
338
  @classmethod
@@ -611,6 +611,26 @@ class Device(BaseModel):
611
611
  version: str
612
612
  MAC: str # pylint: disable=invalid-name
613
613
  uptime: int
614
+ bus: str | None = None
615
+ buswritable: int | bool | None = None
616
+ busaddr: int | None = None
617
+ busdest: int | None = None
618
+ busdevices: list[Any] | None = None
619
+
620
+ @property
621
+ def is_pps_bus(self) -> bool:
622
+ """Return whether the device is connected to a PPS bus."""
623
+ return self.bus is not None and self.bus.upper() == "PPS"
624
+
625
+ @property
626
+ def is_bus_writable(self) -> bool:
627
+ """Return whether BSB-LAN reports global bus writes as enabled."""
628
+ return self.buswritable is None or bool(self.buswritable)
629
+
630
+ @property
631
+ def supports_time_sync(self) -> bool:
632
+ """Return whether normal BSB/LPB time synchronization is supported."""
633
+ return not self.is_pps_bus
614
634
 
615
635
 
616
636
  class Info(BaseModel):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-bsblan
3
- Version: 5.2.0
3
+ Version: 6.0.0
4
4
  Summary: Asynchronous Python client for BSBLAN API
5
5
  Project-URL: Homepage, https://github.com/liudger/python-bsblan
6
6
  Project-URL: Repository, https://github.com/liudger/python-bsblan
@@ -0,0 +1,11 @@
1
+ bsblan/__init__.py,sha256=YD_edvxHL5ocUE2iLWhdinqpsRYWJkVrcds0b6aKp9U,1288
2
+ bsblan/bsblan.py,sha256=-Xwju6B5RcRUDYoMcU--WSn5Gvrjv3j6itDnYCtVZhU,66563
3
+ bsblan/constants.py,sha256=Ln0a-4cgKnr6pjtBaPnpdir2vWQTIZunSbHb552adLE,23769
4
+ bsblan/exceptions.py,sha256=jL7qohIMmuVTsdWBB_trKPg5Yzim6JxaOT13h6EJPlk,1770
5
+ bsblan/models.py,sha256=O8VGJGf0Kpqwo4r0SH4xRBmjC759hlnu8wMR9MM_I8E,21457
6
+ bsblan/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ bsblan/utility.py,sha256=sS0wWJoqvLAHzwaSLIqQEQ-boHsYFLKAHw8KNTSmdX8,5772
8
+ python_bsblan-6.0.0.dist-info/METADATA,sha256=yCAHEUdVrPSjdvpodvhdR3NZUVJz47eGzkMefg3zkBg,8529
9
+ python_bsblan-6.0.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
10
+ python_bsblan-6.0.0.dist-info/licenses/LICENSE.md,sha256=Shv8HPcD1WbZjBPvfb5r3h_cwaPeVaUZMUqU_XQGwGw,1092
11
+ python_bsblan-6.0.0.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- bsblan/__init__.py,sha256=YD_edvxHL5ocUE2iLWhdinqpsRYWJkVrcds0b6aKp9U,1288
2
- bsblan/bsblan.py,sha256=PCNForRgeJFHwZRE_mEcvySQxiYLA2JCIzMJu429Fq4,63034
3
- bsblan/constants.py,sha256=LoMSrEdZj2zohDFxOQ2JT7J4jIH9clXh7sfkNnnjl5A,22864
4
- bsblan/exceptions.py,sha256=jL7qohIMmuVTsdWBB_trKPg5Yzim6JxaOT13h6EJPlk,1770
5
- bsblan/models.py,sha256=qEycc2eVkCK2CXWDXBSqtTyM-to6pH7jX6aKvXeH3R8,20705
6
- bsblan/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- bsblan/utility.py,sha256=sS0wWJoqvLAHzwaSLIqQEQ-boHsYFLKAHw8KNTSmdX8,5772
8
- python_bsblan-5.2.0.dist-info/METADATA,sha256=MjhQBz4cjiRwunT2OH4X-gRFsprNSIpLBMNu7kam24Y,8529
9
- python_bsblan-5.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
10
- python_bsblan-5.2.0.dist-info/licenses/LICENSE.md,sha256=Shv8HPcD1WbZjBPvfb5r3h_cwaPeVaUZMUqU_XQGwGw,1092
11
- python_bsblan-5.2.0.dist-info/RECORD,,