pyhive-integration 2.0.0b1__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 (100) hide show
  1. apyhiveapi/__init__.py +29 -0
  2. apyhiveapi/action.py +15 -0
  3. apyhiveapi/api/__init__.py +1 -0
  4. apyhiveapi/api/device_registration.py +339 -0
  5. apyhiveapi/api/hive_api.py +237 -0
  6. apyhiveapi/api/hive_async_api.py +239 -0
  7. apyhiveapi/api/hive_auth.py +626 -0
  8. apyhiveapi/api/hive_auth_async.py +502 -0
  9. apyhiveapi/api/srp_crypto.py +89 -0
  10. apyhiveapi/boost.py +15 -0
  11. apyhiveapi/color.py +15 -0
  12. apyhiveapi/data/data.json +4064 -0
  13. apyhiveapi/device_attributes.py +15 -0
  14. apyhiveapi/heating.py +15 -0
  15. apyhiveapi/helper/__init__.py +4 -0
  16. apyhiveapi/helper/compat_aliases.py +145 -0
  17. apyhiveapi/helper/const.py +325 -0
  18. apyhiveapi/helper/device_attributes.py +96 -0
  19. apyhiveapi/helper/device_handler_base.py +78 -0
  20. apyhiveapi/helper/hive_exceptions.py +133 -0
  21. apyhiveapi/helper/hive_helper.py +342 -0
  22. apyhiveapi/helper/hivedataclasses.py +113 -0
  23. apyhiveapi/helper/map.py +25 -0
  24. apyhiveapi/hive.py +65 -0
  25. apyhiveapi/hotwater.py +15 -0
  26. apyhiveapi/hub.py +15 -0
  27. apyhiveapi/light.py +15 -0
  28. apyhiveapi/plug.py +15 -0
  29. apyhiveapi/sensor.py +15 -0
  30. apyhiveapi/session_discovery.py +15 -0
  31. apyhiveapi/session_polling.py +15 -0
  32. apyhiveapi/session_tokens.py +15 -0
  33. pyhive/__init__.py +29 -0
  34. pyhive/action.py +15 -0
  35. pyhive/api/__init__.py +1 -0
  36. pyhive/api/device_registration.py +339 -0
  37. pyhive/api/hive_api.py +237 -0
  38. pyhive/api/hive_async_api.py +239 -0
  39. pyhive/api/hive_auth.py +626 -0
  40. pyhive/api/hive_auth_async.py +502 -0
  41. pyhive/api/srp_crypto.py +89 -0
  42. pyhive/boost.py +15 -0
  43. pyhive/color.py +15 -0
  44. pyhive/data/data.json +4064 -0
  45. pyhive/device_attributes.py +15 -0
  46. pyhive/heating.py +15 -0
  47. pyhive/helper/__init__.py +4 -0
  48. pyhive/helper/compat_aliases.py +145 -0
  49. pyhive/helper/const.py +325 -0
  50. pyhive/helper/device_attributes.py +96 -0
  51. pyhive/helper/device_handler_base.py +78 -0
  52. pyhive/helper/hive_exceptions.py +133 -0
  53. pyhive/helper/hive_helper.py +342 -0
  54. pyhive/helper/hivedataclasses.py +113 -0
  55. pyhive/helper/map.py +25 -0
  56. pyhive/hive.py +65 -0
  57. pyhive/hotwater.py +15 -0
  58. pyhive/hub.py +15 -0
  59. pyhive/light.py +15 -0
  60. pyhive/plug.py +15 -0
  61. pyhive/sensor.py +15 -0
  62. pyhive/session_discovery.py +15 -0
  63. pyhive/session_polling.py +15 -0
  64. pyhive/session_tokens.py +15 -0
  65. pyhive_integration-2.0.0b1.dist-info/METADATA +228 -0
  66. pyhive_integration-2.0.0b1.dist-info/RECORD +100 -0
  67. pyhive_integration-2.0.0b1.dist-info/WHEEL +5 -0
  68. pyhive_integration-2.0.0b1.dist-info/licenses/LICENSE +21 -0
  69. pyhive_integration-2.0.0b1.dist-info/top_level.txt +2 -0
  70. pyhiveapi/__init__.py +29 -0
  71. pyhiveapi/action.py +15 -0
  72. pyhiveapi/api/__init__.py +1 -0
  73. pyhiveapi/api/device_registration.py +339 -0
  74. pyhiveapi/api/hive_api.py +237 -0
  75. pyhiveapi/api/hive_async_api.py +239 -0
  76. pyhiveapi/api/hive_auth.py +626 -0
  77. pyhiveapi/api/hive_auth_async.py +502 -0
  78. pyhiveapi/api/srp_crypto.py +89 -0
  79. pyhiveapi/boost.py +15 -0
  80. pyhiveapi/color.py +15 -0
  81. pyhiveapi/device_attributes.py +15 -0
  82. pyhiveapi/heating.py +15 -0
  83. pyhiveapi/helper/__init__.py +4 -0
  84. pyhiveapi/helper/compat_aliases.py +145 -0
  85. pyhiveapi/helper/const.py +325 -0
  86. pyhiveapi/helper/device_attributes.py +96 -0
  87. pyhiveapi/helper/device_handler_base.py +78 -0
  88. pyhiveapi/helper/hive_exceptions.py +133 -0
  89. pyhiveapi/helper/hive_helper.py +342 -0
  90. pyhiveapi/helper/hivedataclasses.py +113 -0
  91. pyhiveapi/helper/map.py +25 -0
  92. pyhiveapi/hive.py +65 -0
  93. pyhiveapi/hotwater.py +15 -0
  94. pyhiveapi/hub.py +15 -0
  95. pyhiveapi/light.py +15 -0
  96. pyhiveapi/plug.py +15 -0
  97. pyhiveapi/sensor.py +15 -0
  98. pyhiveapi/session_discovery.py +15 -0
  99. pyhiveapi/session_polling.py +15 -0
  100. pyhiveapi/session_tokens.py +15 -0
apyhiveapi/__init__.py ADDED
@@ -0,0 +1,29 @@
1
+ """__init__.py."""
2
+
3
+ # pylint: skip-file
4
+ # ruff: noqa
5
+ if __name__ == "pyhiveapi": # pragma: no cover
6
+ from .api.hive_api import HiveApi as API # type: ignore[assignment] # pragma: no cover
7
+ from .api.hive_auth import HiveAuth as Auth # type: ignore[assignment] # pragma: no cover
8
+ else:
9
+ from .api.hive_async_api import HiveApiAsync as API # type: ignore[assignment]
10
+ from .api.hive_auth_async import HiveAuthAsync as Auth # type: ignore[assignment]
11
+
12
+ from .helper.const import SMS_REQUIRED
13
+ from .helper.hive_exceptions import (
14
+ HiveApiError,
15
+ HiveAuthCredentialError,
16
+ HiveAuthError,
17
+ HiveConfigurationError,
18
+ HiveConnectionError,
19
+ HiveError,
20
+ HiveFailedToRefreshTokens,
21
+ HiveInvalid2FACode,
22
+ HiveInvalidDeviceAuthentication,
23
+ HiveInvalidPassword,
24
+ HiveInvalidUsername,
25
+ HiveReauthRequired,
26
+ HiveRefreshTokenExpired,
27
+ HiveUnknownConfiguration,
28
+ )
29
+ from .hive import Hive
apyhiveapi/action.py ADDED
@@ -0,0 +1,15 @@
1
+ """Backwards-compatible shim — use apyhiveapi.devices.action instead."""
2
+
3
+ # pylint: skip-file
4
+ # ruff: noqa: F401, E402
5
+ import warnings
6
+
7
+ warnings.warn(
8
+ "apyhiveapi.action is deprecated; import from apyhiveapi.devices.action",
9
+ DeprecationWarning,
10
+ stacklevel=2,
11
+ )
12
+
13
+ from .devices.action import HiveAction
14
+
15
+ __all__ = ["HiveAction"]
@@ -0,0 +1 @@
1
+ """__init__.py file."""
@@ -0,0 +1,339 @@
1
+ """Device registration and management mixin for HiveAuthAsync."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import datetime
7
+ import functools
8
+ import hashlib
9
+ import hmac
10
+ import logging
11
+ import os
12
+ import re
13
+ import socket
14
+ from typing import Any
15
+
16
+ import botocore
17
+
18
+ from ..helper.hive_exceptions import HiveApiError, HiveInvalid2FACode
19
+ from .srp_crypto import (
20
+ G_HEX,
21
+ N_HEX,
22
+ calculate_u,
23
+ compute_hkdf,
24
+ get_random,
25
+ hash_sha256,
26
+ hex_hash,
27
+ hex_to_long,
28
+ long_to_hex,
29
+ pad_hex,
30
+ )
31
+
32
+ _LOGGER = logging.getLogger(__name__)
33
+
34
+
35
+ class DeviceRegistrationMixin:
36
+ """Device registration, confirmation, and management methods.
37
+
38
+ Expects ``self.client``, ``self.loop``, ``self._client_id``,
39
+ ``self.access_token``, ``self.device_group_key``, ``self.device_key``,
40
+ ``self.device_password``, ``self.k``, ``self.g_value``, ``self.big_n``,
41
+ ``self.small_a_value``, ``self.large_a_value``, and ``self.client_secret``
42
+ to be set up by the owning class's ``__init__`` / ``async_init``.
43
+ """
44
+
45
+ # Attributes provided by HiveAuthAsync.__init__ / async_init
46
+ client: Any
47
+ loop: Any
48
+ _client_id: str | None
49
+ access_token: str | None
50
+ device_group_key: str | None
51
+ device_key: str | None
52
+ device_password: str | None
53
+ k: int
54
+ g_value: int
55
+ big_n: int
56
+ small_a_value: int
57
+ large_a_value: int
58
+ client_secret: str | None
59
+
60
+ def generate_hash_device(self, device_group_key, device_key):
61
+ """Generate device hash key."""
62
+ # source: https://github.com/amazon-archives/amazon-cognito-identity-js/blob/6b87f1a30a998072b4d98facb49dcaf8780d15b0/src/AuthenticationHelper.js#L137 # pylint: disable=line-too-long
63
+
64
+ device_password = base64.standard_b64encode(os.urandom(40)).decode("utf-8")
65
+ combined_string = f"{device_group_key}{device_key}:{device_password}"
66
+ combined_string_hash = hash_sha256(combined_string.encode("utf-8"))
67
+ salt = pad_hex(get_random(16))
68
+
69
+ x_value = hex_to_long(hex_hash(salt + combined_string_hash))
70
+ g_value = hex_to_long(G_HEX)
71
+ big_n = hex_to_long(N_HEX)
72
+ verifier_device_not_padded = pow(g_value, x_value, big_n)
73
+ verifier = pad_hex(verifier_device_not_padded)
74
+
75
+ device_secret_verifier_config = {
76
+ "PasswordVerifier": base64.standard_b64encode(
77
+ bytearray.fromhex(verifier)
78
+ ).decode("utf-8"),
79
+ "Salt": base64.standard_b64encode(bytearray.fromhex(salt)).decode("utf-8"),
80
+ }
81
+ self.device_password = device_password
82
+ return device_secret_verifier_config
83
+
84
+ def get_device_authentication_key( # pylint: disable=too-many-positional-arguments
85
+ self, device_group_key, device_key, device_password, server_b_value, salt
86
+ ):
87
+ """Get device authentication key."""
88
+ u_value = calculate_u(self.large_a_value, server_b_value)
89
+ if u_value == 0:
90
+ raise ValueError("U cannot be zero.")
91
+ username_password = f"{device_group_key}{device_key}:{device_password}"
92
+ username_password_hash = hash_sha256(username_password.encode("utf-8"))
93
+
94
+ x_value = hex_to_long(hex_hash(pad_hex(salt) + username_password_hash))
95
+ g_mod_pow_xn = pow(self.g_value, x_value, self.big_n)
96
+ int_value2 = (server_b_value - self.k * g_mod_pow_xn) % self.big_n
97
+ exp = self.small_a_value + u_value * x_value
98
+ s_value = pow(int_value2, exp, self.big_n)
99
+ hkdf = compute_hkdf(
100
+ bytearray.fromhex(pad_hex(s_value)),
101
+ bytearray.fromhex(pad_hex(long_to_hex(u_value))),
102
+ )
103
+ return hkdf
104
+
105
+ async def process_device_challenge(self, challenge_parameters):
106
+ """Process device challenge."""
107
+ username = challenge_parameters["USERNAME"]
108
+ salt_hex = (
109
+ challenge_parameters["SALT"]
110
+ if isinstance(challenge_parameters["SALT"], str)
111
+ else pad_hex(challenge_parameters["SALT"])
112
+ )
113
+ srp_b_hex = challenge_parameters["SRP_B"]
114
+ secret_block_b64 = challenge_parameters["SECRET_BLOCK"]
115
+ # re strips leading zero from a day number (required by AWS Cognito)
116
+ timestamp = re.sub(
117
+ r" 0(\d) ",
118
+ r" \1 ",
119
+ datetime.datetime.now(datetime.timezone.utc).strftime(
120
+ "%a %b %d %H:%M:%S UTC %Y"
121
+ ),
122
+ )
123
+ hkdf = self.get_device_authentication_key(
124
+ self.device_group_key,
125
+ self.device_key,
126
+ self.device_password,
127
+ hex_to_long(srp_b_hex),
128
+ salt_hex,
129
+ )
130
+ secret_block_bytes = base64.standard_b64decode(secret_block_b64)
131
+ msg = (
132
+ bytearray(self.device_group_key, "utf-8")
133
+ + bytearray(self.device_key, "utf-8")
134
+ + bytearray(secret_block_bytes)
135
+ + bytearray(timestamp, "utf-8")
136
+ )
137
+ hmac_obj = hmac.new(hkdf, msg, digestmod=hashlib.sha256)
138
+ signature_string = base64.standard_b64encode(hmac_obj.digest())
139
+ response = {
140
+ "TIMESTAMP": timestamp,
141
+ "USERNAME": username,
142
+ "PASSWORD_CLAIM_SECRET_BLOCK": secret_block_b64,
143
+ "PASSWORD_CLAIM_SIGNATURE": signature_string.decode("utf-8"),
144
+ "DEVICE_KEY": self.device_key,
145
+ }
146
+ if self.client_secret is not None:
147
+ response.update(
148
+ {
149
+ "SECRET_HASH": self.get_secret_hash(
150
+ username, self._client_id, self.client_secret
151
+ )
152
+ }
153
+ )
154
+ return response
155
+
156
+ async def device_registration(self, device_name: str | None = None):
157
+ """Register device with Hive."""
158
+ _LOGGER.debug("device_registration - Registering device with Hive.")
159
+ await self.confirm_device(device_name)
160
+ await self.update_device_status()
161
+
162
+ async def confirm_device(self, device_name: str | None = None):
163
+ """Confirm Hive Device."""
164
+ if self.client is None:
165
+ await self.async_init() # type: ignore[attr-defined]
166
+
167
+ if device_name is None:
168
+ device_name = socket.gethostname()
169
+
170
+ result = None
171
+ try:
172
+ device_secret_verifier_config = self.generate_hash_device(
173
+ self.device_group_key, self.device_key
174
+ )
175
+ result = await self.loop.run_in_executor(
176
+ None,
177
+ functools.partial(
178
+ self.client.confirm_device,
179
+ AccessToken=self.access_token,
180
+ DeviceKey=self.device_key,
181
+ DeviceName=device_name,
182
+ DeviceSecretVerifierConfig=device_secret_verifier_config,
183
+ ),
184
+ )
185
+ except botocore.exceptions.ClientError as err:
186
+ code = (err.response or {}).get("Error", {}).get("Code", "")
187
+ if code == "CodeMismatchException":
188
+ raise HiveInvalid2FACode from err
189
+ raise HiveApiError from err
190
+ except botocore.exceptions.EndpointConnectionError as err:
191
+ raise HiveApiError from err
192
+
193
+ return result
194
+
195
+ async def update_device_status(self):
196
+ """Update Device Hive."""
197
+ if self.client is None:
198
+ await self.async_init() # type: ignore[attr-defined]
199
+ result = None
200
+ try:
201
+ result = await self.loop.run_in_executor(
202
+ None,
203
+ functools.partial(
204
+ self.client.update_device_status,
205
+ AccessToken=self.access_token,
206
+ DeviceKey=self.device_key,
207
+ DeviceRememberedStatus="remembered",
208
+ ),
209
+ )
210
+ except botocore.exceptions.EndpointConnectionError as err:
211
+ raise HiveApiError from err
212
+
213
+ return result
214
+
215
+ async def get_device_data(self):
216
+ """Get key device information for device authentication.
217
+
218
+ Returns:
219
+ tuple: (device_group_key, device_key, device_password, token_created)
220
+ token_created is a datetime marking when the current tokens were issued.
221
+ Pass all four values as ``device_data`` in ``start_session`` config so the
222
+ session can compute token expiry from the real issue time rather than epoch.
223
+ """
224
+ return (
225
+ self.device_group_key,
226
+ self.device_key,
227
+ self.device_password,
228
+ self.token_created,
229
+ )
230
+
231
+ async def is_device_registered(self, access_token=None, device_key=None):
232
+ """Check if the current device is registered with Cognito.
233
+
234
+ Args:
235
+ access_token (str, optional): Access token. Defaults to self.access_token.
236
+ device_key (str, optional): Device key. Defaults to self.device_key.
237
+
238
+ Returns:
239
+ bool: True if device is registered and remembered, False otherwise.
240
+
241
+ Raises:
242
+ HiveApiError: If unable to reach Cognito endpoint.
243
+ """
244
+ if self.client is None:
245
+ await self.async_init() # type: ignore[attr-defined]
246
+
247
+ token = access_token or self.access_token
248
+ key = device_key or self.device_key
249
+
250
+ if not token or not key:
251
+ _LOGGER.debug(
252
+ "is_device_registered - Missing access token or device key, "
253
+ "device not registered"
254
+ )
255
+ return False
256
+
257
+ _LOGGER.debug(
258
+ "is_device_registered - Checking device registration status for device: %s",
259
+ key,
260
+ )
261
+
262
+ try:
263
+ result = await self.loop.run_in_executor(
264
+ None,
265
+ functools.partial(
266
+ self.client.get_device,
267
+ AccessToken=token,
268
+ DeviceKey=key,
269
+ ),
270
+ )
271
+
272
+ if result and "Device" in result:
273
+ device_status = result["Device"].get("DeviceAttributes", [])
274
+ for attr in device_status:
275
+ if (
276
+ attr.get("Name") == "dev:device_remembered_status"
277
+ and attr.get("Value") == "remembered"
278
+ ):
279
+ _LOGGER.debug(
280
+ "is_device_registered - Device %s is registered and remembered",
281
+ key,
282
+ )
283
+ return True
284
+
285
+ _LOGGER.debug(
286
+ "is_device_registered - Device %s is registered but not remembered",
287
+ key,
288
+ )
289
+
290
+ except botocore.exceptions.ClientError as err:
291
+ error = (err.response or {}).get("Error", {})
292
+ error_code = error.get("Code")
293
+ error_message = error.get("Message", "")
294
+
295
+ if error_code == "ResourceNotFoundException":
296
+ _LOGGER.debug(
297
+ "is_device_registered - Device %s not found in Cognito", key
298
+ )
299
+ elif error_code == "NotAuthorizedException":
300
+ _LOGGER.warning(
301
+ "is_device_registered - Not authorized to check device status: %s",
302
+ error_message,
303
+ )
304
+ else:
305
+ _LOGGER.error(
306
+ "is_device_registered - Error checking device status: %s - %s",
307
+ error_code,
308
+ error_message,
309
+ )
310
+
311
+ except botocore.exceptions.EndpointConnectionError as err:
312
+ _LOGGER.error(
313
+ "is_device_registered - Cannot reach Cognito endpoint: %s", str(err)
314
+ )
315
+ raise HiveApiError from err
316
+
317
+ return False
318
+
319
+ async def forget_device(self, access_token, device_key):
320
+ """Forget device registered with Hive."""
321
+ if self.client is None:
322
+ await self.async_init() # type: ignore[attr-defined]
323
+ result = None
324
+
325
+ try:
326
+ result = await self.loop.run_in_executor(
327
+ None,
328
+ functools.partial(
329
+ self.client.forget_device,
330
+ AccessToken=access_token,
331
+ DeviceKey=device_key,
332
+ ),
333
+ )
334
+ except botocore.exceptions.ClientError as err:
335
+ raise HiveApiError from err
336
+ except botocore.exceptions.EndpointConnectionError as err:
337
+ raise HiveApiError from err
338
+
339
+ return result
@@ -0,0 +1,237 @@
1
+ """Hive API Module."""
2
+
3
+ import json
4
+ import logging
5
+ import re
6
+
7
+ import requests
8
+ from pyquery import PyQuery
9
+
10
+ from ..helper.hive_exceptions import HiveConnectionError
11
+
12
+ _LOGGER = logging.getLogger(__name__)
13
+
14
+ _NO_RESPONSE = "No response to Hive API request"
15
+ _ERROR_RESPONSE = "Error making API call"
16
+
17
+ # requests exceptions all subclass OSError; response.json() raises a
18
+ # json.JSONDecodeError subclass.
19
+ _REQUEST_ERRORS = (OSError, RuntimeError, json.JSONDecodeError)
20
+
21
+ _SSO_ASSIGNMENT = re.compile(r'window\.(\w+)\s*=\s*"([^"]*)"')
22
+
23
+
24
+ class HiveApi:
25
+ """Hive API Code."""
26
+
27
+ def __init__(self, hive_session=None, token=None):
28
+ """Hive API initialisation."""
29
+ self.urls = {
30
+ "properties": "https://sso.hivehome.com/",
31
+ "login": "https://beekeeper.hivehome.com/1.0/cognito/login",
32
+ "refresh": "https://beekeeper.hivehome.com/1.0/cognito/refresh-token",
33
+ "long_lived": "https://api.prod.bgchprod.info/omnia/accessTokens",
34
+ "base": "https://beekeeper-uk.hivehome.com/1.0",
35
+ "weather": "https://weather.prod.bgchprod.info/weather",
36
+ "holiday_mode": "/holiday-mode",
37
+ "all": "/nodes/all?products=true&devices=true&actions=true",
38
+ "devices": "/devices",
39
+ "products": "/products",
40
+ "actions": "/actions",
41
+ "nodes": "/nodes/{0}/{1}",
42
+ }
43
+ self.timeout = 5
44
+ self.json_return = {
45
+ "original": _NO_RESPONSE,
46
+ "parsed": _NO_RESPONSE,
47
+ }
48
+ self.session = hive_session
49
+ self.token = token
50
+ self.headers = {
51
+ "content-type": "application/json",
52
+ "Accept": "*/*",
53
+ "authorization": "",
54
+ }
55
+
56
+ def request(self, http_method, url, jsc=None):
57
+ """Make API request."""
58
+ _LOGGER.debug("request - Making %s request to: %s", http_method, url)
59
+ if jsc:
60
+ _LOGGER.debug("request - Request payload: %s", jsc)
61
+
62
+ if self.session is not None:
63
+ self.headers["authorization"] = self.session.tokens.token_data["token"]
64
+ else:
65
+ self.headers["authorization"] = self.token
66
+
67
+ _LOGGER.debug(
68
+ "request - Request headers: %s",
69
+ {k: v for k, v in self.headers.items() if k.lower() != "authorization"},
70
+ )
71
+
72
+ try:
73
+ if http_method == "GET":
74
+ return requests.get(
75
+ url=url, headers=self.headers, data=jsc, timeout=self.timeout
76
+ )
77
+ if http_method == "POST":
78
+ return requests.post(
79
+ url=url, headers=self.headers, data=jsc, timeout=self.timeout
80
+ )
81
+ if http_method == "DELETE":
82
+ return requests.delete(
83
+ url=url, headers=self.headers, data=jsc, timeout=self.timeout
84
+ )
85
+ raise ValueError(f"Unsupported request type: {http_method}")
86
+ except Exception as e:
87
+ _LOGGER.error("Request failed: %s", e)
88
+ raise
89
+
90
+ def _call_endpoint(self, http_method, url, jsc=None):
91
+ """Call an endpoint and return a fresh result dict for this call."""
92
+ json_return = {
93
+ "original": _NO_RESPONSE,
94
+ "parsed": _NO_RESPONSE,
95
+ }
96
+ try:
97
+ response = self.request(http_method, url, jsc)
98
+ if response is not None:
99
+ json_return["original"] = response.status_code
100
+ json_return["parsed"] = response.json()
101
+ else:
102
+ _LOGGER.error("No response from Hive API call to %s", url)
103
+ except _REQUEST_ERRORS as e:
104
+ _LOGGER.error("Hive API call to %s failed: %s", url, e)
105
+ json_return = self.error()
106
+
107
+ return json_return
108
+
109
+ def get_login_info(self):
110
+ """Get login properties to make the login request."""
111
+ _LOGGER.debug(
112
+ "get_login_info - Fetching login info from: %s", self.urls["properties"]
113
+ )
114
+ url = self.urls["properties"]
115
+ try:
116
+ data = requests.get(url=url, timeout=self.timeout)
117
+ _LOGGER.debug(
118
+ "get_login_info - Login info response status: %s", data.status_code
119
+ )
120
+ script_text = PyQuery(data.content)("script:first").text()
121
+ sso_values = dict(_SSO_ASSIGNMENT.findall(script_text))
122
+
123
+ login_data = {
124
+ "UPID": sso_values["HiveSSOPoolId"],
125
+ "CLIID": sso_values["HiveSSOPublicCognitoClientId"],
126
+ "REGION": sso_values["HiveSSOPoolId"],
127
+ }
128
+ _LOGGER.debug("get_login_info - Login info extracted successfully")
129
+ return login_data
130
+ except OSError as e:
131
+ # requests' timeouts and connection errors are OSError subclasses.
132
+ _LOGGER.error("Failed to get login info: %s", str(e))
133
+ self.error()
134
+ raise HiveConnectionError("Could not reach the Hive login page") from e
135
+ except (RuntimeError, KeyError) as e:
136
+ _LOGGER.error("Failed to get login info: %s", str(e))
137
+ self.error()
138
+ return None
139
+
140
+ def get_all(self):
141
+ """Build and query all endpoint."""
142
+ _LOGGER.debug("get_all - Fetching all devices/products/actions from Hive API")
143
+ url = self.urls["base"] + self.urls["all"]
144
+ return self._call_endpoint("GET", url)
145
+
146
+ def get_devices(self):
147
+ """Call the get devices endpoint."""
148
+ url = self.urls["base"] + self.urls["devices"]
149
+ return self._call_endpoint("GET", url)
150
+
151
+ def get_products(self):
152
+ """Call the get products endpoint."""
153
+ url = self.urls["base"] + self.urls["products"]
154
+ return self._call_endpoint("GET", url)
155
+
156
+ def get_actions(self):
157
+ """Call the get actions endpoint."""
158
+ url = self.urls["base"] + self.urls["actions"]
159
+ return self._call_endpoint("GET", url)
160
+
161
+ def motion_sensor(self, sensor, fromepoch, toepoch):
162
+ """Call a way to get motion sensor info."""
163
+ url = (
164
+ self.urls["base"]
165
+ + self.urls["products"]
166
+ + "/"
167
+ + sensor["type"]
168
+ + "/"
169
+ + sensor["id"]
170
+ + "/events?from="
171
+ + str(fromepoch)
172
+ + "&to="
173
+ + str(toepoch)
174
+ )
175
+ return self._call_endpoint("GET", url)
176
+
177
+ def get_weather(self, weather_url):
178
+ """Call endpoint to get local weather from Hive API."""
179
+ t_url = self.urls["weather"] + weather_url
180
+ url = t_url.replace(" ", "%20")
181
+ return self._call_endpoint("GET", url)
182
+
183
+ def set_state(self, n_type, n_id, **kwargs):
184
+ """Set the state of a Device."""
185
+ _LOGGER.debug(
186
+ "set_state - Setting state for device %s (type: %s): %s",
187
+ n_id,
188
+ n_type,
189
+ kwargs,
190
+ )
191
+ jsc = json.dumps(kwargs)
192
+ url = self.urls["base"] + self.urls["nodes"].format(n_type, n_id)
193
+ return self._call_endpoint("POST", url, jsc)
194
+
195
+ def set_action(self, n_id, data):
196
+ """Set the state of a Action."""
197
+ jsc = data
198
+ url = self.urls["base"] + self.urls["actions"] + "/" + n_id
199
+ return self._call_endpoint("POST", url, jsc)
200
+
201
+ def get_holiday_mode(self):
202
+ """Get the current holiday mode configuration."""
203
+ url = self.urls["base"] + self.urls["holiday_mode"]
204
+ return self._call_endpoint("GET", url)
205
+
206
+ def set_holiday_mode(self, start, end, temperature):
207
+ """Schedule holiday mode.
208
+
209
+ Args:
210
+ start: Start time as epoch milliseconds.
211
+ end: End time as epoch milliseconds.
212
+ temperature: Frost-protection temperature to hold during holiday mode.
213
+ """
214
+ jsc = json.dumps({"start": start, "end": end, "temperature": temperature})
215
+ url = self.urls["base"] + self.urls["holiday_mode"]
216
+ return self._call_endpoint("POST", url, jsc)
217
+
218
+ def cancel_holiday_mode(self):
219
+ """Cancel any scheduled or active holiday mode."""
220
+ url = self.urls["base"] + self.urls["holiday_mode"]
221
+ return self._call_endpoint("DELETE", url, json.dumps({}))
222
+
223
+ def error(self):
224
+ """An error has occurred interacting with the Hive API."""
225
+ _LOGGER.error("API error occurred - returning error response")
226
+ error_return = {
227
+ "original": _ERROR_RESPONSE,
228
+ "parsed": _ERROR_RESPONSE,
229
+ }
230
+ # Kept in sync for backwards compatibility with callers that read
231
+ # the last error state off the instance.
232
+ self.json_return.update(error_return)
233
+ return error_return
234
+
235
+
236
+ class UnknownConfig(Exception):
237
+ """Unknown API config."""