PyViCare 2.59.0__tar.gz → 2.59.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.
Files changed (30) hide show
  1. {pyvicare-2.59.0 → pyvicare-2.59.1}/PKG-INFO +1 -1
  2. {pyvicare-2.59.0 → pyvicare-2.59.1}/PyViCare/PyViCareAbstractOAuthManager.py +34 -3
  3. {pyvicare-2.59.0 → pyvicare-2.59.1}/PyViCare/PyViCareDeviceConfig.py +18 -1
  4. {pyvicare-2.59.0 → pyvicare-2.59.1}/pyproject.toml +1 -1
  5. {pyvicare-2.59.0 → pyvicare-2.59.1}/LICENSE +0 -0
  6. {pyvicare-2.59.0 → pyvicare-2.59.1}/PyViCare/Feature.py +0 -0
  7. {pyvicare-2.59.0 → pyvicare-2.59.1}/PyViCare/PyViCare.py +0 -0
  8. {pyvicare-2.59.0 → pyvicare-2.59.1}/PyViCare/PyViCareBrowserOAuthManager.py +0 -0
  9. {pyvicare-2.59.0 → pyvicare-2.59.1}/PyViCare/PyViCareCachedService.py +0 -0
  10. {pyvicare-2.59.0 → pyvicare-2.59.1}/PyViCare/PyViCareDevice.py +0 -0
  11. {pyvicare-2.59.0 → pyvicare-2.59.1}/PyViCare/PyViCareElectricalEnergySystem.py +0 -0
  12. {pyvicare-2.59.0 → pyvicare-2.59.1}/PyViCare/PyViCareFloorHeating.py +0 -0
  13. {pyvicare-2.59.0 → pyvicare-2.59.1}/PyViCare/PyViCareFuelCell.py +0 -0
  14. {pyvicare-2.59.0 → pyvicare-2.59.1}/PyViCare/PyViCareGateway.py +0 -0
  15. {pyvicare-2.59.0 → pyvicare-2.59.1}/PyViCare/PyViCareGazBoiler.py +0 -0
  16. {pyvicare-2.59.0 → pyvicare-2.59.1}/PyViCare/PyViCareHeatCurveCalculation.py +0 -0
  17. {pyvicare-2.59.0 → pyvicare-2.59.1}/PyViCare/PyViCareHeatPump.py +0 -0
  18. {pyvicare-2.59.0 → pyvicare-2.59.1}/PyViCare/PyViCareHeatingDevice.py +0 -0
  19. {pyvicare-2.59.0 → pyvicare-2.59.1}/PyViCare/PyViCareHybrid.py +0 -0
  20. {pyvicare-2.59.0 → pyvicare-2.59.1}/PyViCare/PyViCareOAuthManager.py +0 -0
  21. {pyvicare-2.59.0 → pyvicare-2.59.1}/PyViCare/PyViCareOilBoiler.py +0 -0
  22. {pyvicare-2.59.0 → pyvicare-2.59.1}/PyViCare/PyViCarePelletsBoiler.py +0 -0
  23. {pyvicare-2.59.0 → pyvicare-2.59.1}/PyViCare/PyViCareRadiatorActuator.py +0 -0
  24. {pyvicare-2.59.0 → pyvicare-2.59.1}/PyViCare/PyViCareRepeater.py +0 -0
  25. {pyvicare-2.59.0 → pyvicare-2.59.1}/PyViCare/PyViCareRoomSensor.py +0 -0
  26. {pyvicare-2.59.0 → pyvicare-2.59.1}/PyViCare/PyViCareService.py +0 -0
  27. {pyvicare-2.59.0 → pyvicare-2.59.1}/PyViCare/PyViCareUtils.py +0 -0
  28. {pyvicare-2.59.0 → pyvicare-2.59.1}/PyViCare/PyViCareVentilationDevice.py +0 -0
  29. {pyvicare-2.59.0 → pyvicare-2.59.1}/PyViCare/__init__.py +0 -0
  30. {pyvicare-2.59.0 → pyvicare-2.59.1}/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyViCare
3
- Version: 2.59.0
3
+ Version: 2.59.1
4
4
  Summary: Library to communicate with the Viessmann ViCare API.
5
5
  License: Apache-2.0
6
6
  License-File: LICENSE
@@ -44,7 +44,9 @@ class AbstractViCareOAuthManager:
44
44
  def get(self, url: str) -> Any:
45
45
  try:
46
46
  logger.debug(self.__oauth)
47
- response = self.__oauth.get(f"{API_BASE_URL}{url}", timeout=31).json()
47
+ raw_response = self.__oauth.get(f"{API_BASE_URL}{url}", timeout=31)
48
+ self.__raise_on_non_json_error(raw_response)
49
+ response = raw_response.json()
48
50
  logger.debug("Response to get request: %s", response)
49
51
  self.__handle_expired_token(response)
50
52
  self.__handle_rate_limit(response)
@@ -58,6 +60,21 @@ class AbstractViCareOAuthManager:
58
60
  except InvalidTokenError:
59
61
  self.renewToken()
60
62
  return self.get(url)
63
+ except OSError as e:
64
+ raise PyViCareInternalServerError(
65
+ {"statusCode": 0,
66
+ "message": str(e),
67
+ "viErrorId": "n/a"}) from e
68
+
69
+ def __raise_on_non_json_error(self, response):
70
+ """Guard against non-JSON error responses (e.g. 502 HTML pages from API gateway)."""
71
+ if response.status_code >= 500:
72
+ content_type = response.headers.get('content-type', '')
73
+ if 'application/json' not in content_type:
74
+ raise PyViCareInternalServerError(
75
+ {"statusCode": response.status_code,
76
+ "message": f"Non-JSON {response.status_code} response",
77
+ "viErrorId": "n/a"})
61
78
 
62
79
  def __handle_expired_token(self, response):
63
80
  if ("error" in response and response["error"] == "EXPIRED TOKEN"):
@@ -82,6 +99,13 @@ class AbstractViCareOAuthManager:
82
99
  if ("statusCode" in response and response["statusCode"] >= 500):
83
100
  raise PyViCareInternalServerError(response)
84
101
 
102
+ extended = response.get("extendedPayload", {})
103
+ if isinstance(extended, dict) and extended.get("code") in ("500", "502", "503"):
104
+ raise PyViCareInternalServerError(
105
+ {"statusCode": int(extended["code"]),
106
+ "message": extended.get("reason", ""),
107
+ "viErrorId": response.get("viErrorId", "n/a")})
108
+
85
109
  def __handle_command_error(self, response):
86
110
  if not Feature.raise_exception_on_command_failure:
87
111
  return
@@ -106,8 +130,10 @@ class AbstractViCareOAuthManager:
106
130
  headers = {"Content-Type": "application/json",
107
131
  "Accept": "application/vnd.siren+json"}
108
132
  try:
109
- response = self.__oauth.post(
110
- f"{API_BASE_URL}{url}", data, headers=headers).json()
133
+ raw_response = self.__oauth.post(
134
+ f"{API_BASE_URL}{url}", data, headers=headers)
135
+ self.__raise_on_non_json_error(raw_response)
136
+ response = raw_response.json()
111
137
  self.__handle_expired_token(response)
112
138
  self.__handle_rate_limit(response)
113
139
  self.__handle_command_error(response)
@@ -118,3 +144,8 @@ class AbstractViCareOAuthManager:
118
144
  except InvalidTokenError:
119
145
  self.renewToken()
120
146
  return self.post(url, data)
147
+ except OSError as e:
148
+ raise PyViCareInternalServerError(
149
+ {"statusCode": 0,
150
+ "message": str(e),
151
+ "viErrorId": "n/a"}) from e
@@ -125,11 +125,28 @@ class PyViCareDeviceConfig:
125
125
  for (creator_method, type_name, roles) in device_types:
126
126
  if re.search(type_name, self.device_model) or self.service.hasRoles(roles):
127
127
  logger.info("detected %s %s", self.device_model, creator_method.__name__)
128
- return creator_method()
128
+ device = creator_method()
129
+ if isinstance(device, (GazBoiler, HeatPump)) and not isinstance(device, Hybrid):
130
+ if self._isHybridByFeatures():
131
+ logger.info("upgrading %s to Hybrid based on API features", self.device_model)
132
+ return self.asHybridDevice()
133
+ return device
129
134
 
130
135
  logger.info("Could not auto detect %s. Use generic device.", self.device_model)
131
136
  return self.asGeneric()
132
137
 
138
+ def _isHybridByFeatures(self):
139
+ """Check API features to detect hybrid devices (both burners and compressors)."""
140
+ try:
141
+ features = self.service.fetch_all_features()
142
+ feature_names = [f["feature"] for f in features.get("data", [])]
143
+ has_burners = any(f.startswith("heating.burners") for f in feature_names)
144
+ has_compressors = any(f.startswith("heating.compressors") for f in feature_names)
145
+ return has_burners and has_compressors
146
+ except (KeyError, TypeError, AttributeError, OSError):
147
+ logger.debug("Could not fetch features for hybrid detection of %s", self.device_model)
148
+ return False
149
+
133
150
  def get_raw_json(self):
134
151
  return self.service.fetch_all_features()
135
152
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "PyViCare"
3
- version = "2.59.0"
3
+ version = "2.59.1"
4
4
  description = "Library to communicate with the Viessmann ViCare API."
5
5
  authors = [
6
6
  "Simon Gillet <mail+pyvicare@gillet.ninja>"
File without changes
File without changes
File without changes