python-aidot 0.3.54b4__py3-none-any.whl → 0.3.55__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.
aidot/api/__init__.py ADDED
File without changes
aidot/api/cloud_api.py ADDED
@@ -0,0 +1,151 @@
1
+ """Cloud API for AiDot HTTP requests."""
2
+
3
+ from collections.abc import Callable
4
+ import logging
5
+ from typing import Any
6
+
7
+ import aiohttp
8
+ from aiohttp import ClientSession
9
+
10
+ from ..const import (
11
+ API_URL_TEMPLATE,
12
+ CONF_ACCESS_TOKEN,
13
+ CONF_CODE,
14
+ CONF_REFRESH_TOKEN,
15
+ ServerErrorCode,
16
+ )
17
+ from ..exceptions import AidotAuthFailed, AidotUserOrPassIncorrect
18
+ from ..models.auth_model import RequestHeaders, UserInformation
19
+
20
+ _LOGGER = logging.getLogger(__name__)
21
+
22
+
23
+ class CloudApi:
24
+ """Cloud API client scoped to one AiDot account."""
25
+
26
+ def __init__(
27
+ self,
28
+ session: ClientSession,
29
+ user_info: UserInformation,
30
+ auth_failed_callback: Callable[[], None] | None = None,
31
+ token_refreshed_callback: Callable[[], None] | None = None,
32
+ ) -> None:
33
+ """Initialize an account-scoped Cloud API client."""
34
+ self._session = session
35
+ self._user_info = user_info
36
+ self._base_url = API_URL_TEMPLATE.format(region=user_info.region)
37
+ self._auth_failed_callback = auth_failed_callback
38
+ self._token_refreshed_callback = token_refreshed_callback
39
+
40
+ def _get_headers(self) -> dict[str, str]:
41
+ """Build request headers with access token."""
42
+ return RequestHeaders.create(self._user_info.accessToken)
43
+
44
+ async def login(self, data: dict[str, Any]) -> dict[str, Any]:
45
+ """Login and return tokens."""
46
+ url = f"{self._base_url}/users/loginWithFreeVerification"
47
+ headers = self._get_headers()
48
+
49
+ response_data = {}
50
+ try:
51
+ _LOGGER.info("POST %s body=%s", url, data)
52
+ response = await self._session.post(url, headers=headers, json=data)
53
+ response_data = await response.json()
54
+ response.raise_for_status()
55
+ _LOGGER.info("POST %s → %s resp=%s", url, response.status, response_data)
56
+ return response_data
57
+ except aiohttp.ClientError as e:
58
+ _LOGGER.error(f"login failed: {e} resp={response_data}")
59
+ if response_data.get(CONF_CODE) == ServerErrorCode.USER_PWD_INCORRECT:
60
+ raise AidotUserOrPassIncorrect
61
+ raise Exception
62
+
63
+ async def refresh_token(self) -> dict[str, Any]:
64
+ """Refresh access token."""
65
+ url = f"{self._base_url}/users/refreshToken"
66
+ headers = self._get_headers()
67
+ data = {
68
+ CONF_REFRESH_TOKEN: self._user_info.refreshToken,
69
+ }
70
+
71
+ response_data = {}
72
+ try:
73
+ _LOGGER.info("POST %s body=%s", url, data)
74
+ response = await self._session.post(url, headers=headers, json=data)
75
+ response_data = await response.json()
76
+ response.raise_for_status()
77
+
78
+ # Update tokens and notify
79
+ self._user_info.accessToken = response_data.get(CONF_ACCESS_TOKEN, "")
80
+ if response_data.get(CONF_REFRESH_TOKEN) is not None:
81
+ self._user_info.refreshToken = response_data[CONF_REFRESH_TOKEN]
82
+
83
+ # Notify callback
84
+ if self._token_refreshed_callback:
85
+ self._token_refreshed_callback()
86
+
87
+ _LOGGER.info("Token refreshed: %s", response_data)
88
+ return response_data
89
+ except aiohttp.ClientError as e:
90
+ _LOGGER.error(f"refresh_token failed: {e} resp={response_data}")
91
+ code = response_data.get(CONF_CODE)
92
+ if code == ServerErrorCode.LOGIN_INVALID or code in (21027, 21041):
93
+ raise AidotAuthFailed
94
+ return None
95
+
96
+ async def get(self, params: str) -> dict[str, Any]:
97
+ """GET request with auto token refresh on 401."""
98
+ url = f"{self._base_url}{params}"
99
+ headers = self._get_headers()
100
+
101
+ response_data = {}
102
+ try:
103
+ _LOGGER.info("GET %s", url)
104
+ response = await self._session.get(url, headers=headers)
105
+ response_data = await response.json()
106
+ response.raise_for_status()
107
+ _LOGGER.info("GET %s → %s", url, response.status)
108
+ return response_data
109
+ except aiohttp.ClientError as e:
110
+ _LOGGER.error(f"GET {params} failed: {e} resp={response_data}")
111
+ code = response_data.get(CONF_CODE)
112
+ if code == ServerErrorCode.TOKEN_EXPIRED:
113
+ try:
114
+ refresh_data = await self.refresh_token()
115
+ if refresh_data:
116
+ _LOGGER.info("Retrying GET %s", url)
117
+ headers = self._get_headers()
118
+ response = await self._session.get(url, headers=headers)
119
+ response_data = await response.json()
120
+ response.raise_for_status()
121
+ _LOGGER.info("GET %s → %s retry=ok", url, response.status)
122
+ return response_data
123
+ except AidotAuthFailed:
124
+ if self._auth_failed_callback:
125
+ self._auth_failed_callback()
126
+ raise
127
+ except Exception as refresh_error:
128
+ _LOGGER.error(f"Token refresh failed: {refresh_error}")
129
+ if self._auth_failed_callback:
130
+ self._auth_failed_callback()
131
+ raise AidotAuthFailed
132
+ elif code in (ServerErrorCode.LOGIN_INVALID, 21027, 21041):
133
+ if self._auth_failed_callback:
134
+ self._auth_failed_callback()
135
+ raise AidotAuthFailed
136
+ raise
137
+
138
+ async def get_products(self, product_ids: str) -> list[dict[str, Any]]:
139
+ """Get products by comma-separated IDs."""
140
+ params = f"/products/{product_ids}"
141
+ return await self.get(params)
142
+
143
+ async def get_devices(self, house_id: str) -> list[dict[str, Any]]:
144
+ """Get devices in a house."""
145
+ params = f"/devices?houseId={house_id}"
146
+ return await self.get(params)
147
+
148
+ async def get_houses(self) -> list[dict[str, Any]]:
149
+ """Get all houses for the user."""
150
+ params = "/houses"
151
+ return await self.get(params)
aidot/client.py CHANGED
@@ -2,74 +2,42 @@
2
2
 
3
3
  import asyncio
4
4
  import logging
5
- import base64
6
- import aiohttp
7
5
  from aiohttp import ClientSession
8
6
  from typing import Any, Optional
9
- from cryptography.hazmat.backends import default_backend
10
- from cryptography.hazmat.primitives import serialization
11
- from cryptography.hazmat.primitives.asymmetric import padding
12
7
  import uuid
13
8
  from pathlib import Path
14
9
  import hashlib
15
- from .exceptions import AidotAuthFailed, AidotUserOrPassIncorrect
10
+
11
+ from .utils import rsa_encrypt
16
12
  from .device_client import DeviceClient
13
+ from .models.auth_model import UserInformation, LoginRequest
14
+ from .models.device_model import DeviceModel
15
+ from .api.cloud_api import CloudApi
17
16
  from .discover import Discover
18
- from .login_const import APP_ID, PUBLIC_KEY_PEM, API_URL_TEMPLATE, DEFAULT_REGION
17
+ from .const import PUBLIC_KEY_PEM
19
18
  from .const import (
20
- CONF_ACCESS_TOKEN,
21
- CONF_APP_ID,
22
- CONF_CODE,
23
- CONF_COUNTRY,
24
- CONF_DEVICE_LIST,
25
19
  CONF_ID,
26
20
  CONF_IPADDRESS,
27
- CONF_PASSWORD,
21
+ CONF_LOGIN_INFO,
22
+ CONF_DEVICE_LIST,
28
23
  CONF_PRODUCT,
29
24
  CONF_PRODUCT_ID,
30
- CONF_REFRESH_TOKEN,
31
- CONF_REGION,
32
- CONF_TERMINAL,
33
- CONF_TOKEN,
34
- CONF_USERNAME,
35
- DEFAULT_COUNTRY_NAME,
36
- SUPPORTED_COUNTRYS,
37
- DEFAULT_COUNTRY_CODE,
38
25
  CONF_IS_OWNER,
26
+ SUPPORTED_COUNTRYS,
39
27
  CONF_TYPE,
40
28
  CONF_AES_KEY,
41
- ServerErrorCode,
42
29
  )
43
30
 
44
31
  _LOGGER = logging.getLogger(__name__)
45
32
 
46
33
 
47
- def rsa_password_encrypt(message: str) -> str:
48
- """Get password rsa encrypt."""
49
- public_key = serialization.load_pem_public_key(
50
- PUBLIC_KEY_PEM, backend=default_backend()
51
- )
52
-
53
- encrypted = public_key.encrypt(
54
- message.encode("utf-8"),
55
- padding.PKCS1v15(),
56
- )
57
-
58
- encrypted_base64 = base64.b64encode(encrypted).decode("utf-8")
59
- return encrypted_base64
60
-
61
-
62
34
  class AidotClient:
63
- _base_url: str = API_URL_TEMPLATE.format(region=DEFAULT_REGION)
64
- _region: str = DEFAULT_REGION
65
- session: Optional[ClientSession] = None
66
- username: str = ""
67
- password: str = ""
68
- country_name: str = DEFAULT_COUNTRY_NAME
69
- country_code: str = DEFAULT_COUNTRY_CODE
70
- login_info: dict[str, Any] = {}
35
+ """AiDot client for managing devices and authentication."""
36
+
71
37
  _device_clients: dict[str, DeviceClient]
72
- _discover: Discover = None
38
+ user_info: UserInformation = None
39
+ _token_fresh_cb: Optional[callable] = None
40
+ _products: dict[str, dict[str, Any]] = {}
73
41
 
74
42
  def __init__(
75
43
  self,
@@ -79,42 +47,55 @@ class AidotClient:
79
47
  password: str | None = None,
80
48
  token: dict | None = None,
81
49
  ) -> None:
82
- _LOGGER.info("Client Version: v0.3.54b3")
83
- self.session = session
84
- self.username = username
85
- self.password = password
50
+ _LOGGER.info("Client Version: v0.3.55")
86
51
  self.country_code = country_code
87
52
  self._device_clients = {}
53
+ self.user_info = UserInformation(
54
+ username=username, password=password, country_code=country_code
55
+ )
56
+
57
+ # Set region and country from country_code
88
58
  for item in SUPPORTED_COUNTRYS:
89
- if item["id"] == self.country_code:
90
- self.country_name = item["name"]
91
- self._region = item["region"].lower()
92
- self._base_url = API_URL_TEMPLATE.format(region=self._region)
59
+ if item[CONF_ID] == self.country_code:
60
+ self.user_info.country = item["name"]
61
+ self.user_info.region = item["region"].lower()
93
62
  break
63
+
64
+ # Handle token (existing login info)
94
65
  if token is not None:
95
- # ✅ 兼容性处理: v1.0.8 数据结构迁移到 v1.1.3
96
- # 旧版本: config_entry.data[CONF_LOGIN_INFO]
97
- # 新版本: config_entry.data
98
66
  if token.get(CONF_ID) is None and token.get(CONF_LOGIN_INFO) is not None:
99
67
  token = token.get(CONF_LOGIN_INFO)
100
-
101
- self.login_info = token.copy()
102
- self.username = token[CONF_USERNAME]
103
- self.password = token[CONF_PASSWORD]
104
- self._region = token[CONF_REGION]
105
- self.country_name = token[CONF_COUNTRY]
106
- self._base_url = API_URL_TEMPLATE.format(region=self._region)
68
+ self.user_info.update_from_json(token)
69
+
70
+ self._cloud_api = CloudApi(
71
+ session=session,
72
+ user_info=self.user_info,
73
+ auth_failed_callback=self._on_auth_failed,
74
+ token_refreshed_callback=self._on_token_refreshed,
75
+ )
107
76
  self.setup_discover()
77
+
78
+ @property
79
+ def login_info(self) -> dict:
80
+ return self.user_info.to_dict()
81
+
82
+ def _on_auth_failed(self) -> None:
83
+ """Handle authentication failed event from CloudApi."""
84
+ _LOGGER.warning("Authentication failed, clearing user info")
85
+ self.user_info.accessToken = ""
86
+
87
+ def _on_token_refreshed(self) -> None:
88
+ """Handle token refreshed event from CloudApi."""
89
+ _LOGGER.debug("Token refreshed successfully")
90
+ if self._token_fresh_cb:
91
+ self._token_fresh_cb()
92
+
108
93
  def set_token_fresh_cb(self, callback) -> None:
94
+ """Set callback for token refresh events."""
109
95
  self._token_fresh_cb = callback
110
96
 
111
- def get_identifier(self) -> str:
112
- return f"{self._region}-{self.username}"
113
-
114
- def update_password(self, password: str) -> None:
115
- self.password = password
116
-
117
97
  async def get_terminal_id(self) -> str:
98
+ """Get or create terminal ID for device identification."""
118
99
  file_path = Path.home() / ".aidot_terminal_id"
119
100
 
120
101
  def _read_or_create() -> str:
@@ -127,188 +108,102 @@ class AidotClient:
127
108
  file_path.write_text(raw_id)
128
109
  return raw_id
129
110
  except OSError:
130
- return 'gvz3gjae10l4zii00t7y0'
111
+ return "gvz3gjae10l4zii00t7y0"
131
112
 
132
113
  raw_id = await asyncio.to_thread(_read_or_create)
133
114
  return hashlib.md5(raw_id.encode()).hexdigest()
134
115
 
135
116
  async def async_post_login(self) -> dict[str, Any]:
136
117
  """Login the user input allows us to connect."""
137
- url = f"{self._base_url}/users/loginWithFreeVerification"
138
- headers = {CONF_APP_ID: APP_ID, CONF_TERMINAL: "app"}
139
- # f"{region}:{self.country_name.strip()}",
140
- terminalId = await self.get_terminal_id()
141
- if terminalId is None:
142
- terminalId = "gvz3gjae10l4zii00t7y0"
143
- data = {
144
- "countryKey": f"region:{self.country_name.strip()}",
145
- "username": self.username,
146
- "password": rsa_password_encrypt(self.password),
147
- "terminalId": terminalId,
148
- "webVersion": "0.5.0",
149
- "area": "Asia/Shanghai",
150
- "UTC": "UTC+8",
151
- }
118
+ terminal_id = await self.get_terminal_id()
152
119
 
153
- try:
154
- response = await self.session.post(url, headers=headers, json=data)
155
- response_data = await response.json()
156
- response.raise_for_status()
157
- self.login_info = response_data
158
- self.login_info[CONF_PASSWORD] = self.password
159
- self.login_info[CONF_REGION] = self._region
160
- self.login_info[CONF_COUNTRY] = self.country_name
161
- self.setup_discover()
162
- return self.login_info
163
- except aiohttp.ClientError as e:
164
- _LOGGER.error(f"async_post_login ClientError: {e}")
165
- if response_data[CONF_CODE] == ServerErrorCode.USER_PWD_INCORRECT:
166
- raise AidotUserOrPassIncorrect
167
- raise Exception
168
-
169
- async def async_refresh_token(self) -> dict[str, Any]:
170
- url = f"{self._base_url}/users/refreshToken"
171
- headers = {CONF_APP_ID: APP_ID, CONF_TERMINAL: "app"}
172
- data = {
173
- CONF_REFRESH_TOKEN: self.login_info[CONF_REFRESH_TOKEN],
174
- }
120
+ login_request = LoginRequest.create(
121
+ username=self.user_info.username,
122
+ encrypted_password=rsa_encrypt(self.user_info.password, PUBLIC_KEY_PEM),
123
+ country_name=self.user_info.country,
124
+ terminal_id=terminal_id,
125
+ )
175
126
 
176
- try:
177
- response = await self.session.post(url, headers=headers, json=data)
178
- response_data = await response.json()
179
- response.raise_for_status()
180
- self.login_info[CONF_ACCESS_TOKEN] = response_data[CONF_ACCESS_TOKEN]
181
- if response_data[CONF_REFRESH_TOKEN] is not None:
182
- self.login_info[CONF_REFRESH_TOKEN] = response_data[CONF_REFRESH_TOKEN]
183
- _LOGGER.debug(f"refresh token: {response_data}")
184
- if self._token_fresh_cb:
185
- self._token_fresh_cb()
186
- return response_data
187
- except aiohttp.ClientError as e:
188
- _LOGGER.error(f"async_refresh_token ClientError: {e} {response_data}")
189
- if response_data[CONF_CODE] == ServerErrorCode.LOGIN_INVALID:
190
- raise AidotAuthFailed
191
- return None
192
-
193
- async def async_session_get(
194
- self, params: str, headers: str | None = None
195
- ) -> dict[str, Any]:
196
- url = f"{self._base_url}{params}"
197
- token = self.login_info[CONF_ACCESS_TOKEN]
198
- if token is None:
199
- raise AidotAuthFailed()
200
- if headers is None:
201
- headers = {
202
- CONF_TERMINAL: "app",
203
- CONF_TOKEN: token,
204
- CONF_APP_ID: APP_ID,
205
- }
206
- response_data = {}
207
- try:
208
- response = await self.session.get(url, headers=headers)
209
- response_data = await response.json()
210
- response.raise_for_status()
211
- return response_data
212
- except aiohttp.ClientError as e:
213
- _LOGGER.error(f"async_get ClientError: {e} {response_data}")
214
- code = response_data.get(CONF_CODE)
215
- if code == ServerErrorCode.TOKEN_EXPIRED:
216
- try:
217
- await self.async_refresh_token()
218
- return await self.async_session_get(params)
219
- except AidotAuthFailed:
220
- raise AidotAuthFailed
221
- elif (
222
- code == ServerErrorCode.LOGIN_INVALID or code == 21027 or code == 21041
223
- ):
224
- self.login_info[CONF_ACCESS_TOKEN] = None
225
- raise AidotAuthFailed
226
- return aiohttp.ClientError
227
-
228
- async def async_get_products(self, product_ids: str) -> list[dict[str, Any]]:
229
- """Get device list."""
230
- params = f"/products/{product_ids}"
231
- return await self.async_session_get(params)
232
-
233
- async def async_get_devices(self, house_id: str) -> list[dict[str, Any]]:
234
- """Get device list."""
235
- params = f"/devices?houseId={house_id}"
236
- return await self.async_session_get(params)
237
-
238
- async def async_get_houses(self) -> list[dict[str, Any]]:
239
- """Get house list."""
240
- params = "/houses"
241
- return await self.async_session_get(params)
127
+ response_data = await self._cloud_api.login(login_request.to_dict())
128
+ self.user_info.update_from_json(response_data)
129
+ self.setup_discover()
130
+ return self.user_info.to_dict()
242
131
 
243
132
  async def async_get_all_device(self) -> dict[str, Any]:
244
- final_device_list: list[dict[str, Any]] = []
133
+ """Get all devices for the user."""
134
+ filter_devices: dict[str, Any] = {}
135
+ filter_product_ids: set[str] = set()
245
136
  try:
246
- houses = await self.async_get_houses()
137
+ houses = await self._cloud_api.get_houses() or []
247
138
  for house in houses:
248
139
  if house.get(CONF_IS_OWNER) is False:
249
140
  continue
250
- # get device_list
251
- device_list = await self.async_get_devices(house[CONF_ID])
252
- if device_list:
253
- final_device_list.extend(device_list)
254
-
255
- # get product_list
256
- productIds = ",".join([item[CONF_PRODUCT_ID] for item in final_device_list])
257
- product_list = await self.async_get_products(productIds)
258
-
259
- for product in product_list:
260
- for device in final_device_list:
261
- if device[CONF_PRODUCT_ID] == product[CONF_ID]:
262
- device[CONF_PRODUCT] = product
263
-
141
+ device_list = await self._cloud_api.get_devices(house[CONF_ID]) or []
142
+ for device in device_list:
143
+ filter_device: dict[str, Any] = None
144
+ if (
145
+ device.get(CONF_TYPE) == "light"
146
+ and device.get(CONF_AES_KEY, [None])[0] is not None
147
+ ):
148
+ filter_device = device
149
+ if filter_device is not None:
150
+ filter_devices[device[CONF_ID]] = device
151
+ filter_product_ids.add(device[CONF_PRODUCT_ID])
152
+
153
+ # Get product info and merge into devices
154
+ if filter_product_ids:
155
+ product_ids = ",".join(filter_product_ids)
156
+ product_list = await self._cloud_api.get_products(product_ids) or []
157
+ product_map = {p[CONF_ID]: p for p in product_list}
158
+ for device in filter_devices.values():
159
+ device[CONF_PRODUCT] = product_map.get(device[CONF_PRODUCT_ID])
160
+
264
161
  except Exception as e:
265
162
  raise e
266
- return {CONF_DEVICE_LIST: final_device_list}
163
+ return filter_devices
267
164
 
268
165
  def get_device_client(self, device: dict[str, Any]) -> DeviceClient:
269
- device_id = device.get(CONF_ID)
270
- device_client: DeviceClient = self._device_clients.get(device_id)
166
+ """Get or create device client for a device."""
167
+ _device: DeviceModel = DeviceModel.from_json(data=device)
168
+ device_client: DeviceClient = self._device_clients.get(_device.id)
271
169
  if device_client is None:
272
- device_client = DeviceClient(device, self.login_info)
273
- self._device_clients[device_id] = device_client
274
- if self._discover is not None:
275
- ip = self._discover.discovered_device.get(device_id)
276
- device_client.update_ip_address(ip)
170
+ device_client = DeviceClient(_device, self.user_info)
171
+ self._device_clients[_device.id] = device_client
172
+
173
+ ip = Discover.DISCOVERED_DEVICE.get(_device.id)
174
+ device_client.update_ip_address(ip)
277
175
  return device_client
278
176
 
279
177
  async def remove_device_client(self, dev_id: str) -> None:
178
+ """Remove and close device client."""
280
179
  device_client: DeviceClient = self._device_clients.get(dev_id)
281
180
  if device_client is not None:
282
181
  await device_client.close()
283
182
  del self._device_clients[dev_id]
284
183
 
285
184
  def setup_discover(self) -> None:
286
- """初始化完成后调用,启动设备发现"""
287
- if self.login_info.get(CONF_ID) is None:
288
- return
289
- if self._discover is not None:
185
+ """Initialize device discovery after login."""
186
+ if not self.user_info.id:
290
187
  return
291
188
 
292
- _LOGGER.warning(f"setup_discover")
293
- def _discover_callback(dev_id, event: dict[str, str]) -> None:
189
+ _LOGGER.warning("setup_discover")
190
+
191
+ def _discover_callback(dev_id: str, event: dict[str, str]) -> None:
294
192
  device_ip = event[CONF_IPADDRESS]
295
193
  device_client: DeviceClient = self._device_clients.get(dev_id)
296
194
  if device_client is not None:
297
195
  device_client.update_ip_address(device_ip)
298
196
 
299
- self._discover = Discover(self.login_info, _discover_callback)
300
- self._discover.start_repeat_broadcast()
197
+ Discover.set_call_back(_discover_callback)
198
+ Discover.set_user_info(self.user_info)
301
199
 
302
200
  async def async_close(self) -> None:
303
- """关闭客户端,清理资源"""
304
- if self._discover is not None:
305
- self._discover.close()
306
- self._discover = None
201
+ """Close client and cleanup resources."""
307
202
  for client in self._device_clients.values():
308
203
  await client.close()
309
204
  self._device_clients.clear()
310
205
 
311
206
  async def async_cleanup(self) -> None:
312
- """清理所有资源"""
207
+ """Cleanup all resources."""
313
208
  _LOGGER.debug("async_cleanup")
314
209
  await self.async_close()
aidot/const.py CHANGED
@@ -230,3 +230,17 @@ class ServerErrorCode(IntEnum):
230
230
  TOKEN_EXPIRED = 21026
231
231
  LOGIN_INVALID = 21025
232
232
  USER_PWD_INCORRECT = 560080
233
+
234
+
235
+ # Login / API constants
236
+ APP_ID = "1383974540041977857"
237
+ API_URL_TEMPLATE = "https://prod-{region}-api.arnoo.com/v17"
238
+ DEFAULT_REGION = "us"
239
+ PUBLIC_KEY_PEM = b"""
240
+ -----BEGIN PUBLIC KEY-----
241
+ MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCtQAnPCi8ksPnS1Du6z96PsKfN
242
+ p2Gp/f/bHwlrAdplbX3p7/TnGpnbJGkLq8uRxf6cw+vOthTsZjkPCF7CatRvRnTj
243
+ c9fcy7yE0oXa5TloYyXD6GkxgftBbN/movkJJGQCc7gFavuYoAdTRBOyQoXBtm0m
244
+ kXMSjXOldI/290b9BQIDAQAB
245
+ -----END PUBLIC KEY-----
246
+ """