Skip to content

Commit 793464c

Browse files
authored
Scenes (#133)
* Add response object for scenes testing. * Split base device into two classes. * Support scenes * Added scenes to tests. * Bump version number
1 parent 14a6490 commit 793464c

File tree

19 files changed

+476
-160
lines changed

19 files changed

+476
-160
lines changed

.devcontainer.json

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,21 @@
2424
"source.fixAll.ruff": true,
2525
"source.organizeImports": true
2626
},
27-
"editor.defaultFormatter": "ms-python.black-formatter",
28-
"editor.formatOnPaste": false,
27+
"editor.defaultFormatter": "charliermarsh.ruff",
28+
"editor.formatOnPaste": true,
2929
"editor.formatOnSave": true,
3030
"editor.formatOnType": true
3131
},
3232
"[yaml]": {
3333
"editor.defaultFormatter": "esbenp.prettier-vscode"
3434
},
35+
"mypy-type-checker.args": ["--config-file=pyproject.toml"],
36+
"mypy-type-checker.reportingScope": "workspace",
37+
"mypy-type-checker.importStrategy": "useBundled",
3538
"python.analysis.autoSearchPaths": false,
3639
"python.analysis.extraPaths": ["/workspaces/pyalarmdotcomajax"],
37-
"python.formatting.provider": "black",
38-
"python.linting.enabled": true,
3940
"python.languageServer": "Pylance",
4041
"python.testing.pytestEnabled": true,
41-
"python.linting.mypyEnabled": false,
42-
// "python.linting.pylintEnabled": true,
4342
"python.analysis.exclude": ["/**/.*/", "**/__pycache__"],
4443
"python.analysis.diagnosticMode": "workspace",
4544
"python.analysis.typeCheckingMode": "off",

.vscode/tasks.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@
1212
"problemMatcher": [],
1313
"type": "shell"
1414
},
15+
{
16+
"command": "pip install --editable /workspaces/pyalarmdotcomajax --config-settings editable_mode=strict",
17+
"label": "Install pyalarmdotcomajax in editable mode",
18+
"problemMatcher": [],
19+
"type": "shell"
20+
}
1521
],
1622
"version": "2.0.0"
1723
}

pyalarmdotcomajax/__init__.py

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
)
4949
from pyalarmdotcomajax.websockets.client import WebSocketClient, WebSocketState
5050

51-
__version__ = "0.5.8"
51+
__version__ = "0.5.9"
5252

5353
log = logging.getLogger(__name__)
5454

@@ -98,6 +98,8 @@ class AlarmController:
9898
KEEP_ALIVE_SIGNAL_INTERVAL_S = 60
9999
SESSION_REFRESH_DEFAULT_INTERVAL_MS = 780000 # 13 minutes. Sessions expire at 15.
100100

101+
SCENE_REFRESH_INTERVAL_M = 60
102+
101103
# LOGIN & SESSION: END
102104

103105
def __init__(
@@ -161,6 +163,13 @@ def __init__(
161163
self._last_session_refresh: datetime = datetime.now()
162164
self._session_timer: SessionTimer | None = None
163165

166+
#
167+
# SCENE REFRESH ATTRIBUTES
168+
#
169+
170+
self._last_scene_update: datetime | None = None
171+
self._scene_object_cache: list[dict] = []
172+
164173
#
165174
# CLI ATTRIBUTES
166175
#
@@ -230,7 +239,10 @@ async def async_update(self) -> None:
230239

231240
if not self._active_system_id:
232241
self._active_system_id = await self._async_get_active_system()
233-
has_image_sensors = await self._async_has_image_sensors(self._active_system_id)
242+
has_image_sensors = await self._async_device_type_present(
243+
self._active_system_id, DeviceType.IMAGE_SENSOR
244+
)
245+
has_scenes = await self._async_device_type_present(self._active_system_id, DeviceType.SCENE)
234246

235247
await self._async_get_trouble_conditions()
236248

@@ -248,6 +260,21 @@ async def async_update(self) -> None:
248260

249261
extension_results = await self._async_update__query_multi_device_extensions(raw_devices)
250262

263+
#
264+
# QUERY SCENES
265+
#
266+
# Scenes have no state, so we only need to update for new/deleted scenes. We refresh less frequently than we do for stateful devices to save time.
267+
268+
if has_scenes:
269+
# Refresh scene cache if stale.
270+
if not self._last_scene_update or (
271+
datetime.now() > self._last_scene_update + timedelta(minutes=self.SCENE_REFRESH_INTERVAL_M)
272+
):
273+
self._scene_object_cache = await self._async_get_devices_by_device_type(DeviceType.SCENE)
274+
self._last_scene_update = datetime.now()
275+
276+
raw_devices.extend(self._scene_object_cache)
277+
251278
#
252279
# QUERY IMAGE SENSORS
253280
#
@@ -259,8 +286,7 @@ async def async_update(self) -> None:
259286

260287
if has_image_sensors:
261288
# Get detailed image sensor data and add to raw device list.
262-
image_sensors = await self._async_get_devices_by_device_type(DeviceType.IMAGE_SENSOR)
263-
raw_devices.extend(image_sensors)
289+
raw_devices.extend(await self._async_get_devices_by_device_type(DeviceType.IMAGE_SENSOR))
264290

265291
# Get recent images
266292
device_type_specific_data = await self._async_get_recent_images()
@@ -276,7 +302,7 @@ async def async_update(self) -> None:
276302
for partition_raw in raw_devices
277303
if partition_raw["type"] == AttributeRegistry.get_relationship_id_from_devicetype(DeviceType.PARTITION)
278304
]:
279-
partition_instance: AllDevices_t = await self._async_update__build_device(
305+
partition_instance: AllDevices_t = await self._async_update__build_hardware_device(
280306
partition_raw, device_type_specific_data, extension_results
281307
)
282308

@@ -287,13 +313,15 @@ async def async_update(self) -> None:
287313

288314
raw_devices.remove(partition_raw)
289315

316+
# device_type: DeviceType = AttributeRegistry.get_devicetype_from_relationship_id(raw_device["type"])
317+
290318
#
291319
# BUILD DEVICES
292320
#
293321

294322
for device_raw in raw_devices:
295323
try:
296-
device_instance: AllDevices_t = await self._async_update__build_device(
324+
device_instance: AllDevices_t = await self._async_update__build_hardware_device(
297325
device_raw, device_type_specific_data, extension_results
298326
)
299327

@@ -785,7 +813,7 @@ async def _reload_session_context(self) -> None:
785813

786814
self._last_session_refresh = datetime.now()
787815

788-
async def _async_update__build_device(
816+
async def _async_update__build_hardware_device(
789817
self,
790818
raw_device: dict,
791819
device_type_specific_data: dict[str, DeviceTypeSpecificData],
@@ -819,7 +847,7 @@ async def _async_update__build_device(
819847
children.append((sub_device["id"], DeviceType(family_name)))
820848

821849
#
822-
# BUILD DEVICE INSTANCE
850+
# BUILD HARDWARE DEVICE INSTANCE
823851
#
824852

825853
entity_id = raw_device["id"]
@@ -831,8 +859,8 @@ async def _async_update__build_device(
831859
raw_device_data=raw_device,
832860
user_profile=self._user_profile,
833861
children=children,
834-
device_type_specific_data=device_type_specific_data.get(entity_id),
835862
send_action_callback=self.async_send_command,
863+
device_type_specific_data=device_type_specific_data.get(entity_id),
836864
config_change_callback=(
837865
extension_controller.submit_change
838866
if (extension_controller := device_extension_results.get("controller"))
@@ -979,18 +1007,20 @@ async def _async_get_recent_images(self) -> dict[str, DeviceTypeSpecificData]:
9791007
else:
9801008
return device_type_specific_data
9811009

982-
async def _async_has_image_sensors(self, system_id: str, retry_on_failure: bool = True) -> bool:
983-
"""Check whether image sensors are present in system.
1010+
async def _async_device_type_present(
1011+
self, system_id: str, device_type: DeviceType, retry_on_failure: bool = True
1012+
) -> bool:
1013+
"""Check whether a specific device type is present in system.
9841014
985-
Check is required because image sensors are not shown in the device catalog endpoint.
1015+
Check is required because some devices are not shown in the device catalog endpoint.
9861016
"""
9871017

9881018
# TODO: Needs changes to support multi-system environments
9891019

9901020
try:
991-
log.info(f"Checking system {system_id} for image sensors.")
1021+
log.info(f"Checking system {system_id} for {device_type}s.")
9921022

993-
# Find image sensors.
1023+
# Find devices.
9941024

9951025
async with self._websession.get(
9961026
url=AttributeRegistry.get_endpoints(DeviceType.SYSTEM)["primary"].format(c.URL_BASE, system_id),
@@ -1000,14 +1030,16 @@ async def _async_has_image_sensors(self, system_id: str, retry_on_failure: bool
10001030

10011031
await self._async_handle_server_errors(json_rsp, "image sensors", retry_on_failure)
10021032

1003-
return len(json_rsp["data"].get("relationships", {}).get("imageSensors", {}).get("data", [])) > 0
1033+
device_type_id = AttributeRegistry.get_type_id_from_devicetype(device_type)
1034+
1035+
return len(json_rsp["data"].get("relationships", {}).get(device_type_id, {}).get("data", [])) > 0
10041036

10051037
except (aiohttp.ClientResponseError, KeyError) as err:
1006-
log.exception("Failed to get image sensors.")
1038+
log.exception(f"Failed to get {device_type}s.")
10071039
raise UnexpectedResponse from err
10081040
except TryAgain:
10091041
if retry_on_failure:
1010-
return await self._async_has_image_sensors(system_id, retry_on_failure=False)
1042+
return await self._async_device_type_present(system_id, device_type, retry_on_failure=False)
10111043

10121044
raise
10131045

pyalarmdotcomajax/cli.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from pyalarmdotcomajax.devices.registry import AllDevices_t, AttributeRegistry
2727

2828
from . import AlarmController
29-
from .devices import BaseDevice, DeviceType
29+
from .devices import BaseDevice, BatteryState, DeviceType
3030
from .devices.sensor import Sensor
3131
from .exceptions import (
3232
AuthenticationFailed,
@@ -322,6 +322,9 @@ async def cli() -> None:
322322
cprint(f"Unable to find a device with ID {device_id}.", "red")
323323
sys.exit(0)
324324

325+
if not hasattr(device, "settings"):
326+
return
327+
325328
try:
326329
config_option: ConfigurationOption = device.settings[setting_slug]
327330
except KeyError:
@@ -483,26 +486,26 @@ def _print_element_tearsheet(
483486

484487
output_str += "\n"
485488

486-
# BATTERY
487-
if element.battery_critical:
488-
battery = "Critical"
489-
elif element.battery_low:
490-
battery = "Low"
491-
else:
492-
battery = "Normal"
493-
494489
# ATTRIBUTES
495490
output_str += "ATTRIBUTES: "
496491

497-
if isinstance(element.device_subtype, Sensor.Subtype) or element.state or battery or element.read_only:
492+
has_battery = element.battery_state is not BatteryState.NO_BATTERY
493+
494+
if (
495+
isinstance(element.device_subtype, Sensor.Subtype)
496+
or element.state
497+
or has_battery
498+
or element.read_only
499+
or isinstance(element.attributes, BaseDevice.DeviceAttributes)
500+
):
498501
if isinstance(element.device_subtype, Sensor.Subtype):
499502
output_str += f'[TYPE: {element.device_subtype.name.title().replace("_"," ")}] '
500503

501504
if element.state:
502505
output_str += f"[STATE: {element.state.name.title()}] "
503506

504-
if battery:
505-
output_str += f"[BATTERY: {battery}] "
507+
if has_battery:
508+
output_str += f'[BATTERY: {element.battery_state.name.title().replace("_"," ")}] '
506509

507510
if element.read_only:
508511
output_str += f"[READ ONLY: {element.read_only}] "

0 commit comments

Comments
 (0)