pyimouapi 1.2.4__tar.gz → 1.2.6__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.
- {pyimouapi-1.2.4 → pyimouapi-1.2.6}/PKG-INFO +1 -1
- {pyimouapi-1.2.4 → pyimouapi-1.2.6}/pyimouapi/__init__.py +2 -0
- {pyimouapi-1.2.4 → pyimouapi-1.2.6}/pyimouapi/const.py +9 -1
- {pyimouapi-1.2.4 → pyimouapi-1.2.6}/pyimouapi/device.py +28 -22
- {pyimouapi-1.2.4 → pyimouapi-1.2.6}/pyimouapi/exceptions.py +4 -2
- {pyimouapi-1.2.4 → pyimouapi-1.2.6}/pyimouapi/ha_device.py +61 -9
- {pyimouapi-1.2.4 → pyimouapi-1.2.6}/pyimouapi/openapi.py +39 -20
- {pyimouapi-1.2.4 → pyimouapi-1.2.6}/pyimouapi.egg-info/PKG-INFO +1 -1
- {pyimouapi-1.2.4 → pyimouapi-1.2.6}/pyimouapi.egg-info/SOURCES.txt +1 -0
- pyimouapi-1.2.6/pyproject.toml +3 -0
- {pyimouapi-1.2.4 → pyimouapi-1.2.6}/setup.py +1 -1
- {pyimouapi-1.2.4 → pyimouapi-1.2.6}/LICENSE +0 -0
- {pyimouapi-1.2.4 → pyimouapi-1.2.6}/README.md +0 -0
- {pyimouapi-1.2.4 → pyimouapi-1.2.6}/pyimouapi.egg-info/dependency_links.txt +0 -0
- {pyimouapi-1.2.4 → pyimouapi-1.2.6}/pyimouapi.egg-info/requires.txt +0 -0
- {pyimouapi-1.2.4 → pyimouapi-1.2.6}/pyimouapi.egg-info/top_level.txt +0 -0
- {pyimouapi-1.2.4 → pyimouapi-1.2.6}/setup.cfg +0 -0
|
@@ -413,7 +413,15 @@ SENSOR_TYPE_REF = {
|
|
|
413
413
|
"expression": "('e1' if data['14603']==0 else 'e2') if data['14603'] != 1 else int(data['14602'] / data['14601'] * 100)",
|
|
414
414
|
}
|
|
415
415
|
],
|
|
416
|
-
"battery": [
|
|
416
|
+
"battery": [
|
|
417
|
+
{"ref": "11600", "default": "15", "ref_type": "properties"},
|
|
418
|
+
{
|
|
419
|
+
"ref": "106200",
|
|
420
|
+
"default": "0",
|
|
421
|
+
"ref_type": "properties",
|
|
422
|
+
"expression": "battery_106200(data)",
|
|
423
|
+
},
|
|
424
|
+
],
|
|
417
425
|
"temperature_current": [
|
|
418
426
|
{"ref": "16000", "default": "10", "ref_type": "properties"}
|
|
419
427
|
],
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
1
5
|
from .const import (
|
|
2
6
|
API_ENDPOINT_LIST_DEVICE_DETAILS,
|
|
3
7
|
PARAM_PAGE_SIZE,
|
|
@@ -137,7 +141,7 @@ class ImouDevice:
|
|
|
137
141
|
return self._device_status
|
|
138
142
|
|
|
139
143
|
@property
|
|
140
|
-
def channels(self) -> []:
|
|
144
|
+
def channels(self) -> list[ImouChannel]:
|
|
141
145
|
return self._channels
|
|
142
146
|
|
|
143
147
|
@property
|
|
@@ -157,15 +161,15 @@ class ImouDevice:
|
|
|
157
161
|
return self._device_version
|
|
158
162
|
|
|
159
163
|
@property
|
|
160
|
-
def product_id(self) -> str:
|
|
164
|
+
def product_id(self) -> str | None:
|
|
161
165
|
return self._product_id
|
|
162
166
|
|
|
163
167
|
@property
|
|
164
|
-
def parent_product_id(self) -> str:
|
|
168
|
+
def parent_product_id(self) -> str | None:
|
|
165
169
|
return self._parent_product_id
|
|
166
170
|
|
|
167
171
|
@property
|
|
168
|
-
def parent_device_id(self) -> str:
|
|
172
|
+
def parent_device_id(self) -> str | None:
|
|
169
173
|
return self._parent_device_id
|
|
170
174
|
|
|
171
175
|
@property
|
|
@@ -185,7 +189,7 @@ class ImouDevice:
|
|
|
185
189
|
def set_product_id(self, product_id: str) -> None:
|
|
186
190
|
self._product_id = product_id
|
|
187
191
|
|
|
188
|
-
def set_channels(self, channels: []) -> None:
|
|
192
|
+
def set_channels(self, channels: list[ImouChannel]) -> None:
|
|
189
193
|
self._channels = channels
|
|
190
194
|
|
|
191
195
|
def set_channel_number(self, channel_number: int):
|
|
@@ -311,7 +315,7 @@ class ImouDeviceManager:
|
|
|
311
315
|
|
|
312
316
|
async def async_get_device_status(
|
|
313
317
|
self, device_id: str, channel_id: str, enable_type: str
|
|
314
|
-
) -> dict[
|
|
318
|
+
) -> dict[str, Any]:
|
|
315
319
|
"""obtain device capability switch status"""
|
|
316
320
|
params = {
|
|
317
321
|
PARAM_DEVICE_ID: device_id,
|
|
@@ -322,7 +326,7 @@ class ImouDeviceManager:
|
|
|
322
326
|
API_ENDPOINT_GET_DEVICE_STATUS, params
|
|
323
327
|
)
|
|
324
328
|
|
|
325
|
-
async def async_get_device_online_status(self, device_id: str) -> dict[
|
|
329
|
+
async def async_get_device_online_status(self, device_id: str) -> dict[str, Any]:
|
|
326
330
|
"""GET DEVICE ONLINE STATUS"""
|
|
327
331
|
params = {
|
|
328
332
|
PARAM_DEVICE_ID: device_id,
|
|
@@ -346,7 +350,7 @@ class ImouDeviceManager:
|
|
|
346
350
|
|
|
347
351
|
async def async_get_device_night_vision_mode(
|
|
348
352
|
self, device_id: str, channel_id: str
|
|
349
|
-
) -> dict[
|
|
353
|
+
) -> dict[str, Any]:
|
|
350
354
|
"""obtain device night vision mode"""
|
|
351
355
|
params = {
|
|
352
356
|
PARAM_DEVICE_ID: device_id,
|
|
@@ -371,7 +375,7 @@ class ImouDeviceManager:
|
|
|
371
375
|
API_ENDPOINT_SET_DEVICE_NIGHT_VISION_MODE, params
|
|
372
376
|
)
|
|
373
377
|
|
|
374
|
-
async def async_get_device_storage(self, device_id: str) -> dict[
|
|
378
|
+
async def async_get_device_storage(self, device_id: str) -> dict[str, Any]:
|
|
375
379
|
"""obtain device storage media capacity information"""
|
|
376
380
|
params = {PARAM_DEVICE_ID: device_id}
|
|
377
381
|
return await self._imou_api_client.async_request_api(
|
|
@@ -387,7 +391,7 @@ class ImouDeviceManager:
|
|
|
387
391
|
|
|
388
392
|
async def async_get_stream_url(
|
|
389
393
|
self, device_id: str, channel_id: str
|
|
390
|
-
) -> dict[
|
|
394
|
+
) -> dict[str, Any]:
|
|
391
395
|
"""obtain the hls stream address of the device"""
|
|
392
396
|
params = {PARAM_DEVICE_ID: device_id, PARAM_CHANNEL_ID: channel_id}
|
|
393
397
|
return await self._imou_api_client.async_request_api(
|
|
@@ -396,7 +400,7 @@ class ImouDeviceManager:
|
|
|
396
400
|
|
|
397
401
|
async def async_get_device_snap(
|
|
398
402
|
self, device_id: str, channel_id: str
|
|
399
|
-
) -> dict[
|
|
403
|
+
) -> dict[str, Any]:
|
|
400
404
|
params = {PARAM_DEVICE_ID: device_id, PARAM_CHANNEL_ID: channel_id}
|
|
401
405
|
return await self._imou_api_client.async_request_api(
|
|
402
406
|
API_ENDPOINT_SET_DEVICE_SNAP, params
|
|
@@ -404,7 +408,7 @@ class ImouDeviceManager:
|
|
|
404
408
|
|
|
405
409
|
async def async_create_stream_url(
|
|
406
410
|
self, device_id: str, channel_id: str, stream_id: int = 0
|
|
407
|
-
) -> dict[
|
|
411
|
+
) -> dict[str, Any]:
|
|
408
412
|
"""create device hls stream address"""
|
|
409
413
|
params = {
|
|
410
414
|
PARAM_DEVICE_ID: device_id,
|
|
@@ -425,8 +429,12 @@ class ImouDeviceManager:
|
|
|
425
429
|
)
|
|
426
430
|
|
|
427
431
|
async def async_get_iot_device_properties(
|
|
428
|
-
self,
|
|
429
|
-
|
|
432
|
+
self,
|
|
433
|
+
device_id: str,
|
|
434
|
+
channel_id: str | None,
|
|
435
|
+
product_id: str,
|
|
436
|
+
properties: list[Any],
|
|
437
|
+
) -> dict[str, Any]:
|
|
430
438
|
params = {
|
|
431
439
|
PARAM_DEVICE_LIST: [
|
|
432
440
|
{
|
|
@@ -462,15 +470,15 @@ class ImouDeviceManager:
|
|
|
462
470
|
API_ENDPOINT_SET_IOT_DEVICE_PROPERTIES, params
|
|
463
471
|
)
|
|
464
472
|
|
|
465
|
-
async def async_get_device_sd_card_status(self, device_id: str) -> dict[
|
|
473
|
+
async def async_get_device_sd_card_status(self, device_id: str) -> dict[str, Any]:
|
|
466
474
|
params = {PARAM_DEVICE_ID: device_id}
|
|
467
475
|
return await self._imou_api_client.async_request_api(
|
|
468
476
|
API_ENDPOINT_DEVICE_SD_CARD_STATUS, params
|
|
469
477
|
)
|
|
470
478
|
|
|
471
479
|
async def async_iot_device_control(
|
|
472
|
-
self, device_id: str, product_id: str, ref: str, content: dict
|
|
473
|
-
) -> dict[str,
|
|
480
|
+
self, device_id: str, product_id: str, ref: str, content: dict[str, Any]
|
|
481
|
+
) -> dict[str, Any]:
|
|
474
482
|
params = {
|
|
475
483
|
PARAM_DEVICE_ID: device_id,
|
|
476
484
|
PARAM_PRODUCT_ID: product_id,
|
|
@@ -481,7 +489,7 @@ class ImouDeviceManager:
|
|
|
481
489
|
API_ENDPOINT_IOT_DEVICE_CONTROL, params
|
|
482
490
|
)
|
|
483
491
|
|
|
484
|
-
async def async_get_device_power_info(self, device_id: str) -> dict[
|
|
492
|
+
async def async_get_device_power_info(self, device_id: str) -> dict[str, Any]:
|
|
485
493
|
params = {
|
|
486
494
|
PARAM_DEVICE_ID: device_id,
|
|
487
495
|
}
|
|
@@ -498,9 +506,7 @@ class ImouDeviceManager:
|
|
|
498
506
|
API_ENDPOINT_WAKE_UP_DEVICE, params
|
|
499
507
|
)
|
|
500
508
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
async def async_get_product_model(self, product_id: str) -> dict[any, any]:
|
|
509
|
+
async def async_get_product_model(self, product_id: str) -> dict[str, Any]:
|
|
504
510
|
params = {
|
|
505
511
|
PARAM_PRODUCT_ID: product_id,
|
|
506
512
|
}
|
|
@@ -510,7 +516,7 @@ class ImouDeviceManager:
|
|
|
510
516
|
|
|
511
517
|
async def async_get_iot_device_detail_info(
|
|
512
518
|
self, device_id: str, product_id: str
|
|
513
|
-
) -> dict[
|
|
519
|
+
) -> dict[str, Any]:
|
|
514
520
|
params = {
|
|
515
521
|
PARAM_DEVICE_ID: device_id,
|
|
516
522
|
PARAM_PRODUCT_ID: product_id,
|
|
@@ -14,8 +14,10 @@ class ImouException(Exception):
|
|
|
14
14
|
|
|
15
15
|
def traceback(self) -> str:
|
|
16
16
|
"""Return the traceback as a string."""
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
exc_info = sys.exc_info()
|
|
18
|
+
if exc_info[0] is None:
|
|
19
|
+
return ""
|
|
20
|
+
return "".join(traceback.format_exception(*exc_info))
|
|
19
21
|
|
|
20
22
|
def get_title(self) -> str:
|
|
21
23
|
"""Return the title of the exception which will be then translated."""
|
|
@@ -69,6 +69,53 @@ from simpleeval import SimpleEval
|
|
|
69
69
|
|
|
70
70
|
_LOGGER: logging.Logger = logging.getLogger(__package__)
|
|
71
71
|
|
|
72
|
+
|
|
73
|
+
def _battery_level_from_106200_list(data) -> int:
|
|
74
|
+
"""解析 ref 106200 电量属性: [{"106202": 电池类型, "106203": 电量}, ...]。
|
|
75
|
+
多条时取 106202==0 的 106203;仅一条时取该条的 106203。
|
|
76
|
+
键均为字符串。
|
|
77
|
+
"""
|
|
78
|
+
if not isinstance(data, list) or not data:
|
|
79
|
+
return 0
|
|
80
|
+
|
|
81
|
+
k_type, k_level = "106202", "106203"
|
|
82
|
+
|
|
83
|
+
def _to_int(v):
|
|
84
|
+
if v is None:
|
|
85
|
+
return None
|
|
86
|
+
if isinstance(v, bool):
|
|
87
|
+
return int(v)
|
|
88
|
+
if isinstance(v, int):
|
|
89
|
+
return v
|
|
90
|
+
if isinstance(v, float):
|
|
91
|
+
return int(v)
|
|
92
|
+
try:
|
|
93
|
+
return int(str(v).strip())
|
|
94
|
+
except ValueError:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
if len(data) == 1:
|
|
98
|
+
row = data[0]
|
|
99
|
+
if not isinstance(row, dict):
|
|
100
|
+
return 0
|
|
101
|
+
level = _to_int(row.get(k_level))
|
|
102
|
+
return level if level is not None else 0
|
|
103
|
+
|
|
104
|
+
for row in data:
|
|
105
|
+
if not isinstance(row, dict):
|
|
106
|
+
continue
|
|
107
|
+
if _to_int(row.get(k_type)) == 0:
|
|
108
|
+
level = _to_int(row.get(k_level))
|
|
109
|
+
if level is not None:
|
|
110
|
+
return level
|
|
111
|
+
|
|
112
|
+
row0 = data[0]
|
|
113
|
+
if not isinstance(row0, dict):
|
|
114
|
+
return 0
|
|
115
|
+
level = _to_int(row0.get(k_level))
|
|
116
|
+
return level if level is not None else 0
|
|
117
|
+
|
|
118
|
+
|
|
72
119
|
NUMBER_TYPE = [
|
|
73
120
|
PARAM_STORAGE_USED,
|
|
74
121
|
PARAM_TEMPERATURE_CURRENT,
|
|
@@ -323,10 +370,8 @@ class ImouHaDeviceManager(object):
|
|
|
323
370
|
return await self._async_get_device_exist_stream(
|
|
324
371
|
device, live_resolution, live_protocol
|
|
325
372
|
)
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
else:
|
|
329
|
-
raise exception
|
|
373
|
+
raise ex
|
|
374
|
+
raise exception
|
|
330
375
|
|
|
331
376
|
async def _async_get_device_exist_stream(
|
|
332
377
|
self, device: ImouHaDevice, resolution: str, protocol: str
|
|
@@ -352,8 +397,9 @@ class ImouHaDeviceManager(object):
|
|
|
352
397
|
_LOGGER.debug(f"wait {wait_seconds} seconds to download a picture")
|
|
353
398
|
await asyncio.sleep(wait_seconds)
|
|
354
399
|
try:
|
|
355
|
-
|
|
356
|
-
|
|
400
|
+
timeout = aiohttp.ClientTimeout(total=120)
|
|
401
|
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
402
|
+
response = await session.get(data[PARAM_URL])
|
|
357
403
|
if response.status != 200:
|
|
358
404
|
raise RequestFailedException(
|
|
359
405
|
f"request failed,status code {response.status}"
|
|
@@ -413,9 +459,15 @@ class ImouHaDeviceManager(object):
|
|
|
413
459
|
return devices
|
|
414
460
|
|
|
415
461
|
@staticmethod
|
|
416
|
-
def get_expression_value(expression: str, data
|
|
462
|
+
def get_expression_value(expression: str, data):
|
|
417
463
|
s = SimpleEval(
|
|
418
|
-
names={"data": data},
|
|
464
|
+
names={"data": data},
|
|
465
|
+
functions={
|
|
466
|
+
"round": round,
|
|
467
|
+
"int": int,
|
|
468
|
+
"str": str,
|
|
469
|
+
"battery_106200": _battery_level_from_106200_list,
|
|
470
|
+
},
|
|
419
471
|
)
|
|
420
472
|
return s.eval(expression)
|
|
421
473
|
|
|
@@ -1041,7 +1093,7 @@ class ImouHaDeviceManager(object):
|
|
|
1041
1093
|
device_id, device.channel_id, device.product_id, [value[PARAM_REF]]
|
|
1042
1094
|
)
|
|
1043
1095
|
data = result[PARAM_PROPERTIES][value[PARAM_REF]]
|
|
1044
|
-
if value.get(PARAM_EXPRESSION) and isinstance(data, dict):
|
|
1096
|
+
if value.get(PARAM_EXPRESSION) and isinstance(data, (dict, list)):
|
|
1045
1097
|
state = self.get_expression_value(value[PARAM_EXPRESSION], data)
|
|
1046
1098
|
else:
|
|
1047
1099
|
state = data
|
|
@@ -5,8 +5,10 @@ import logging
|
|
|
5
5
|
import secrets
|
|
6
6
|
import time
|
|
7
7
|
import uuid
|
|
8
|
+
from typing import Any
|
|
8
9
|
|
|
9
10
|
import aiohttp
|
|
11
|
+
from urllib.parse import urlparse
|
|
10
12
|
|
|
11
13
|
from .const import (
|
|
12
14
|
API_ENDPOINT_ACCESS_TOKEN,
|
|
@@ -40,28 +42,49 @@ _LOGGER: logging.Logger = logging.getLogger(__package__)
|
|
|
40
42
|
|
|
41
43
|
|
|
42
44
|
class ImouOpenApiClient:
|
|
45
|
+
"""Async client for Imou Open Platform HTTP API."""
|
|
46
|
+
|
|
43
47
|
def __init__(self, app_id: str, app_secret: str, api_url: str) -> None:
|
|
44
48
|
self._app_id = app_id
|
|
45
49
|
self._app_secret = app_secret
|
|
46
50
|
self._api_url = api_url
|
|
47
|
-
|
|
48
|
-
self.
|
|
51
|
+
self._access_token: str | None = None
|
|
52
|
+
self._session: aiohttp.ClientSession | None = None
|
|
53
|
+
|
|
54
|
+
async def _async_get_session(self) -> aiohttp.ClientSession:
|
|
55
|
+
if self._session is None or self._session.closed:
|
|
56
|
+
self._session = aiohttp.ClientSession(
|
|
57
|
+
headers={"Client-Type": "HomeAssistant"},
|
|
58
|
+
)
|
|
59
|
+
return self._session
|
|
60
|
+
|
|
61
|
+
async def async_close(self) -> None:
|
|
62
|
+
"""Close the HTTP session (call when done with the client)."""
|
|
63
|
+
if self._session is not None and not self._session.closed:
|
|
64
|
+
await self._session.close()
|
|
65
|
+
self._session = None
|
|
49
66
|
|
|
50
67
|
async def async_get_token(self) -> None:
|
|
51
|
-
"""
|
|
68
|
+
"""Fetch and store accessToken."""
|
|
52
69
|
response = await self.async_request_api(API_ENDPOINT_ACCESS_TOKEN, {})
|
|
53
70
|
self._access_token = response[PARAM_ACCESS_TOKEN]
|
|
54
71
|
if PARAM_CURRENT_DOMAIN in response:
|
|
55
|
-
|
|
72
|
+
raw = response[PARAM_CURRENT_DOMAIN]
|
|
73
|
+
if "://" not in raw:
|
|
74
|
+
raw = f"https://{raw}"
|
|
75
|
+
parsed = urlparse(raw)
|
|
76
|
+
if parsed.netloc:
|
|
77
|
+
self._api_url = parsed.netloc
|
|
56
78
|
|
|
57
79
|
async def async_request_api(
|
|
58
|
-
self, endpoint: str, params: dict[
|
|
59
|
-
) -> dict[
|
|
60
|
-
|
|
80
|
+
self, endpoint: str, params: dict[str, Any] | None = None
|
|
81
|
+
) -> dict[str, Any]:
|
|
82
|
+
"""POST to an API endpoint; returns the result data object."""
|
|
83
|
+
payload = dict(params) if params else {}
|
|
61
84
|
if self._access_token is None and endpoint != API_ENDPOINT_ACCESS_TOKEN:
|
|
62
85
|
await self.async_get_token()
|
|
63
86
|
if endpoint != API_ENDPOINT_ACCESS_TOKEN:
|
|
64
|
-
|
|
87
|
+
payload[PARAM_TOKEN] = self._access_token
|
|
65
88
|
timestamp = round(time.time())
|
|
66
89
|
nonce = secrets.token_urlsafe()
|
|
67
90
|
sign = hashlib.md5(
|
|
@@ -70,7 +93,7 @@ class ImouOpenApiClient:
|
|
|
70
93
|
)
|
|
71
94
|
).hexdigest()
|
|
72
95
|
request_id = str(uuid.uuid4())
|
|
73
|
-
headers = {"Content-Type": "application/json"
|
|
96
|
+
headers = {"Content-Type": "application/json"}
|
|
74
97
|
body = {
|
|
75
98
|
PARAM_SYSTEM: {
|
|
76
99
|
PARAM_VER: "1.0",
|
|
@@ -79,20 +102,16 @@ class ImouOpenApiClient:
|
|
|
79
102
|
PARAM_TIME: timestamp,
|
|
80
103
|
PARAM_NONCE: nonce,
|
|
81
104
|
},
|
|
82
|
-
PARAM_PARAMS:
|
|
105
|
+
PARAM_PARAMS: payload,
|
|
83
106
|
PARAM_ID: request_id,
|
|
84
107
|
}
|
|
85
108
|
url = f"https://{self._api_url}{endpoint}"
|
|
109
|
+
session = await self._async_get_session()
|
|
86
110
|
try:
|
|
87
|
-
async with
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
)
|
|
92
|
-
response_body = json.loads(await response.text())
|
|
93
|
-
_LOGGER.debug(
|
|
94
|
-
f"url: {url} request body: {body} response: {response_body}"
|
|
95
|
-
)
|
|
111
|
+
async with asyncio.timeout(30):
|
|
112
|
+
response = await session.request("POST", url, json=body, headers=headers)
|
|
113
|
+
response_body = json.loads(await response.text())
|
|
114
|
+
_LOGGER.debug("url: %s request body: %s response: %s", url, body, response_body)
|
|
96
115
|
except Exception as exception:
|
|
97
116
|
raise ConnectFailedException(f"connect failed,{exception}") from exception
|
|
98
117
|
if response.status != 200:
|
|
@@ -117,5 +136,5 @@ class ImouOpenApiClient:
|
|
|
117
136
|
return response_data
|
|
118
137
|
|
|
119
138
|
@property
|
|
120
|
-
def access_token(self):
|
|
139
|
+
def access_token(self) -> str | None:
|
|
121
140
|
return self._access_token
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|