Skip to content

Commit

Permalink
Tidy up event handling. (#116)
Browse files Browse the repository at this point in the history
Stop using deprecated API calls.
  • Loading branch information
twrecked authored Feb 7, 2023
1 parent e365ece commit 77c202b
Show file tree
Hide file tree
Showing 7 changed files with 221 additions and 78 deletions.
3 changes: 3 additions & 0 deletions changelog
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
0.8.0b6: Simplify state checking; bring API up to date and remove deprecated
calls
Add back the packet docs.
0.8.0b5: Add initial 8-in-1 sensor support
[thanks xirtamoen for lending the sensors]
Support Arlo v4 APIs
Expand Down
123 changes: 123 additions & 0 deletions docs/packets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@

# Arlo Packet Types
These are the packets we can receive over the _SSE_ or _MQTT_ back ends.

## Packet type #1
This is a subscription reply packet. This will be received about once a
minute for devices that need them.

```json
{ "action": "is",
"from": "XXXXXXXXXXXXX",
"properties": {"devices": ["XXXXXXXXXXXXX"]},
"resource": "subscriptions/XXXXXXXXXXXXX24993_web",
"to": "XXXXXXXXXXXXX24993_web",
"transId": "web!33c2027d-9b96-4a9f-9b41-aaf412082e80"}
```

## Packet type #2
A base has changed its alarm mode, ie, gone from `disarmed` to `armed`. The
packet can appear when we change mode or another user changes the mode.

```json
{ "4R068BXXXXXXX": { "activeModes": ["mode1"],
"activeSchedules": [],
"timestamp": 1568142116238},
"resource": "activeAutomations"}
```

## Packet type #3
These packets are updates from individual devices, they normally indicate some
sort of activity we are interested in; motion or sound or a temperature change.

```json
{ "action": "is",
"from": "XXXXXXXXXXXXX",
"properties": {"motionDetected": "True"},
"resource": "cameras/XXXXXXXXXXXXX",
"transId": "XXXXXXXXXXXXX!c87fdfa6!1675735611287"}
```

## Packet type #4
These packets are returned from base stations to describe themselves and their
child devices' states. We will periodically ask for this information to keep
our device information up to do.

```json
{ "action": "is",
"devices": { "XXXXXXXXXXXXX": { "properties": { "activityState": "idle",
"alsReading": 32,
"alsSensitivity": 15,
"armed": "True",
"batteryLevel": 45,
"batteryTech": "Rechargeable",
"brightness": 0,
"chargeNotificationLedEnable": "False",
"chargerTech": "None",
"chargingState": "Off",
"colorMode": "single",
"connectionState": "available",
"duration": 300,
"flash": "off",
"hwVersion": "AL1101r3",
"interfaceVersion": 2,
"lampState": "off",
"modelId": "AL1101",
"motionDetected": "False",
"motionSetupModeEnabled": "False",
"motionSetupModeSensitivity": 80,
"multi": { "color1": "0xFF0008",
"color2": "0x23FF02",
"color3": "0x2100FF",
"cycle": 2},
"name": "",
"pattern": "flood",
"sensitivity": 80,
"serialNumber": "XXXXXXXXXXXXX",
"signalStrength": 0,
"single": "0xFFDEAD",
"sleepTime": 0,
"sleepTimeRel": 0,
"swVersion": "3.2.51",
"updateAvailable": "None"},
"states": { "motionStart": { "enabled": "True",
"external": {},
"lightOn": { "brightness": 255,
"colorMode": "white",
"duration": 30,
"enabled": "True",
"flash": "off",
"pattern": "flood"},
"pushNotification": { "enabled": "False"},
"sendEmail": { "enabled": "False",
"recipients": [ ]},
"sensitivity": 80},
"schemaVersion": 1}},
"XXXXXXXXXXXXYY": { "properties": { "antiFlicker": { "autoDefault": 1,
"mode": 0},
"apiVersion": 1,
"autoUpdateEnabled": "True",
"capabilities": ["bridge"],
"claimed": "True",
"connectivity": [ { "connected": "True",
"ipAddr": "192.168.1.179",
"signalStrength": 4,
"ssid": "sprinterland",
"type": "wifi"}],
"hwVersion": "ABB1000r1.0",
"interfaceVersion": 2,
"mcsEnabled": "True",
"modelId": "ABB1000",
"olsonTimeZone": "America/New_York",
"state": "idle",
"swVersion": "2.0.1.0_278_341",
"timeSyncState": "synchronized",
"timeZone": "EST5EDT,M3.2.0,M11.1.0",
"updateAvailable": "None"},
"states": {}}},
"from": "XXXXXXXXXXXXX",
"resource": "devices",
"to": "XXXXXXXXXXXXX24993_web",
"transId": "web!c989b294-b117-4e5e-8647-bb039d9ff8d6"}
```

113 changes: 48 additions & 65 deletions pyaarlo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@

_LOGGER = logging.getLogger("pyaarlo")

__version__ = "0.8.0b5"
__version__ = "0.8.0b6"


class PyArlo(object):
Expand Down Expand Up @@ -262,7 +262,7 @@ def __init__(self, **kwargs):
# Initial config and state retrieval.
if self._cfg.synchronous_mode:
# Synchronous; run them one after the other
self.debug("aarlo: getting initial settings")
self.debug("getting initial settings")
self._refresh_bases(initial=True)
self._refresh_modes()
self._refresh_ambient_sensors()
Expand All @@ -273,7 +273,7 @@ def __init__(self, **kwargs):
self._initial_refresh_done()
else:
# Asynchronous; queue them to run one after the other
self.debug("aarlo: queueing initial settings")
self.debug("queueing initial settings")
self._bg.run(self._refresh_bases, initial=True)
self._bg.run(self._refresh_modes)
self._bg.run(self._refresh_ambient_sensors)
Expand All @@ -284,17 +284,17 @@ def __init__(self, **kwargs):
self._bg.run(self._initial_refresh_done)

# Register house keeping cron jobs.
self.debug("aarlo: registering cron jobs")
self.debug("registering cron jobs")
self._bg.run_every(self._fast_refresh, FAST_REFRESH_INTERVAL)
self._bg.run_every(self._slow_refresh, SLOW_REFRESH_INTERVAL)

# Wait for initial refresh
if self._cfg.wait_for_initial_setup:
with self._lock:
while not self._started:
self.debug("aarlo: waiting for initial setup...")
self.debug("waiting for initial setup...")
self._lock.wait(1)
self.debug("aarlo: setup finished...")
self.debug("setup finished...")

def __repr__(self):
# Representation string of object.
Expand All @@ -307,32 +307,30 @@ def _v3_modes(self):
return self.cfg.mode_api.lower() == "v3"

def _refresh_devices(self):
"""Read in the devices list.
This returns all devices known to the Arlo system. The newer devices
include state information - battery levels etc - while the old devices
don't. We update what we can.
"""
url = DEVICES_PATH + "?t={}".format(time_to_arlotime())
self._devices = self._be.get(url)
if not self._devices:
self.warning("No devices returned from " + url)
self._devices = []
self.vdebug("aarlo: devices={}".format(pprint.pformat(self._devices)))
self.vdebug(f"devices={pprint.pformat(self._devices)}")

# Newer devices include information in this response. Be sure to update it.
for device in self._devices:
device_id = device.get("deviceId", None)
props = device.get("properties", None)
if device_id is None or props is None:
continue
self.debug(f"aarlo: updating {device_id} from device refresh")
base = self.lookup_base_station_by_id(device_id)
if base is not None:
base.update_resources(props)
camera = self.lookup_camera_by_id(device_id)
if camera is not None:
camera.update_resources(props)
doorbell = self.lookup_doorbell_by_id(device_id)
if doorbell is not None:
doorbell.update_resources(props)
light = self.lookup_light_by_id(device_id)
if light is not None:
light.update_resources(props)
self.vdebug(f"device-id={device_id}")
if device_id is not None and props is not None:
device = self.lookup_device_by_id(device_id)
if device is not None:
self.vdebug(f"updating {device_id} from device refresh")
device.update_resources(props)
else:
self.vdebug(f"not updating {device_id} from device refresh")

def _refresh_locations(self):
"""Retrieve location list from the backend
Expand Down Expand Up @@ -381,45 +379,16 @@ def _ping_bases(self):
if base.has_capability(PING_CAPABILITY):
base.ping()
else:
self.vdebug(f"aarlo: NO ping to {base.device_id}")
self.vdebug(f"NO ping to {base.device_id}")

def _refresh_bases(self, initial):
for base in self._bases:
base.update_modes(initial)
base.keep_ratls_open()
if base.has_capability(RESOURCE_CAPABILITY):
self._be.notify(
base=base,
body={
"action": "get",
"resource": "cameras",
"publishResponse": False,
},
wait_for="response",
)
self._be.notify(
base=base,
body={
"action": "get",
"resource": "doorbells",
"publishResponse": False,
},
wait_for="response",
)
self._be.notify(
base=base,
body={
"action": "get",
"resource": "lights",
"publishResponse": False,
},
wait_for="response",
)
else:
self.vdebug(f"aarlo: NO resource for {base.device_id}")
base.update_states()

def _refresh_modes(self):
self.vdebug("aarlo: refresh modes")
self.vdebug("refresh modes")
for base in self._bases:
base.update_modes()
base.update_mode()
Expand All @@ -428,7 +397,7 @@ def _refresh_modes(self):
location.update_mode()

def _fast_refresh(self):
self.vdebug("aarlo: fast refresh")
self.vdebug("fast refresh")
self._bg.run(self._st.save)
self._ping_bases()

Expand All @@ -439,11 +408,11 @@ def _fast_refresh(self):
"mode reload check {} {}".format(str(now), str(self._refresh_modes_at))
)
if now > self._refresh_modes_at:
self.debug("aarlo: mode reload needed")
self.debug("mode reload needed")
self._refresh_modes_at = now + self._cfg.refresh_modes_every
self._bg.run(self._refresh_modes)
else:
self.vdebug("aarlo: no mode reload")
self.vdebug("no mode reload")

# do we need to reload the devices?
if self._cfg.refresh_devices_every != 0:
Expand All @@ -454,34 +423,34 @@ def _fast_refresh(self):
)
)
if now > self._refresh_devices_at:
self.debug("aarlo: device reload needed")
self.debug("device reload needed")
self._refresh_devices_at = now + self._cfg.refresh_devices_every
self._bg.run(self._refresh_devices)
else:
self.vdebug("aarlo: no device reload")
self.vdebug("no device reload")

# if day changes then reload recording library and camera counts
today = datetime.date.today()
self.vdebug("aarlo: day testing with {}!".format(str(today)))
self.vdebug("day testing with {}!".format(str(today)))
if self._today != today:
self.debug("aarlo: day changed to {}!".format(str(today)))
self.debug("day changed to {}!".format(str(today)))
self._today = today
self._bg.run(self._ml.load)
self._bg.run(self._refresh_camera_media, wait=False)

def _slow_refresh(self):
self.vdebug("aarlo: slow refresh")
self.vdebug("slow refresh")
self._bg.run(self._refresh_bases, initial=False)
self._bg.run(self._refresh_ambient_sensors)

def _initial_refresh(self):
self.debug("aarlo: initial refresh")
self.debug("initial refresh")
self._bg.run(self._refresh_bases, initial=True)
self._bg.run(self._refresh_ambient_sensors)
self._bg.run(self._initial_refresh_done)

def _initial_refresh_done(self):
self.debug("aarlo: initial refresh done")
self.debug("initial refresh done")
with self._lock:
self._started = True
self._lock.notify_all()
Expand Down Expand Up @@ -587,6 +556,10 @@ def locations(self):
"""
return self._locations

@property
def all_devices(self):
return self.cameras + self.doorbells + self.lights + self.base_stations + self.locations

@property
def sensors(self):
return self._sensors
Expand Down Expand Up @@ -696,6 +669,16 @@ def lookup_base_station_by_name(self, name):
return base_station[0]
return None

def lookup_device_by_id(self, device_id):
device = self.lookup_base_station_by_id(device_id)
if device is None:
device = self.lookup_camera_by_id(device_id)
if device is None:
device = self.lookup_doorbell_by_id(device_id)
if device is None:
device = self.lookup_light_by_id(device_id)
return device

def inject_response(self, response):
"""Inject a test packet into the event stream.
Expand All @@ -704,7 +687,7 @@ def inject_response(self, response):
:param response: packet to inject.
:type response: JSON data
"""
self.debug("aarlo: injecting\n{}".format(pprint.pformat(response)))
self.debug("injecting\n{}".format(pprint.pformat(response)))
self._be.ev_inject(response)

def attribute(self, attr):
Expand Down
Loading

0 comments on commit 77c202b

Please sign in to comment.