Skip to content

Commit

Permalink
Merge 0.7 (#115)
Browse files Browse the repository at this point in the history
* Add location support.
Add 8-in-1 sensor support.
  • Loading branch information
twrecked authored Feb 5, 2023
1 parent 03c99b4 commit e365ece
Show file tree
Hide file tree
Showing 11 changed files with 642 additions and 148 deletions.
4 changes: 4 additions & 0 deletions changelog
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
0.8.0b5: Add initial 8-in-1 sensor support
[thanks xirtamoen for lending the sensors]
Support Arlo v4 APIs
[thanks JeffSteinbok for the implementation]
0.8.0b4: Allow ping when devices are on chargers.
Added event_id and time to URL paths.
Added custom cipher list.
Expand Down
62 changes: 60 additions & 2 deletions pyaarlo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,20 @@
TOTAL_BELLS_KEY,
TOTAL_CAMERAS_KEY,
TOTAL_LIGHTS_KEY,
LOCATIONS_PATH_FORMAT,
LOCATIONS_EMERGENCY_PATH,
)
from .doorbell import ArloDoorBell
from .light import ArloLight
from .media import ArloMediaLibrary
from .storage import ArloStorage
from .location import ArloLocation
from .sensor import ArloSensor
from .util import time_to_arlotime

_LOGGER = logging.getLogger("pyaarlo")

__version__ = "0.8.0b4"
__version__ = "0.8.0b5"


class PyArlo(object):
Expand Down Expand Up @@ -119,7 +123,7 @@ class PyArlo(object):
* **user_agent** - Set what 'user-agent' string is passed in request headers. It affects what video stream type is
returned. Default is `arlo`.
* **mode_api** - Which api to use to set the base station modes. Default is `auto` which choose an API
based on camera model. Can also be `v1` and `v2`.
based on camera model. Can also be `v1`, `v2` or `v3`. Use `v3` for the new location API.
* **reconnect_every** - Time, in minutes, to close and relogin to Arlo.
* **snapshot_timeout** - Time, in seconds, to stop the snapshot attempt and return the camera to the idle state.
Expand Down Expand Up @@ -167,10 +171,12 @@ def __init__(self, **kwargs):
return

self._lock = threading.Condition()
self._locations = []
self._bases = []
self._cameras = []
self._lights = []
self._doorbells = []
self._sensors = []

# On day flip we do extra work, record today.
self._today = datetime.date.today()
Expand All @@ -185,9 +191,12 @@ def __init__(self, **kwargs):
self._blank_image = base64.standard_b64decode(BLANK_IMAGE)

# Slow piece.
# Get locations for multi location sites.
# Get devices, fill local db, and create device instance.
self.info("pyaarlo starting")
self._started = False
if self._be.multi_location:
self._refresh_locations()
self._refresh_devices()

for device in self._devices:
Expand All @@ -200,6 +209,7 @@ def __init__(self, **kwargs):
# This needs it's own code now... Does no parent indicate a base station???
if (
dtype == "basestation"
or dtype.lower() == 'hub'
or device.get("modelId") == "ABC1000"
or device.get("modelId").startswith(MODEL_GO)
or dtype == "arloq"
Expand Down Expand Up @@ -235,6 +245,8 @@ def __init__(self, **kwargs):
self._doorbells.append(ArloDoorBell(dname, self, device))
if dtype == "lights":
self._lights.append(ArloLight(dname, self, device))
if dtype == "sensors":
self._sensors.append(ArloSensor(dname, self, device))

# Save out unchanging stats!
self._st.set(["ARLO", TOTAL_CAMERAS_KEY], len(self._cameras), prefix="aarlo")
Expand Down Expand Up @@ -288,6 +300,12 @@ def __repr__(self):
# Representation string of object.
return "<{0}: {1}>".format(self.__class__.__name__, self._cfg.name)

# Using this to indicate that we're using location-based modes, vs basestation-based modes.
# also called Arlo app v4. Open to new ideas for what to call this.
@property
def _v3_modes(self):
return self.cfg.mode_api.lower() == "v3"

def _refresh_devices(self):
url = DEVICES_PATH + "?t={}".format(time_to_arlotime())
self._devices = self._be.get(url)
Expand Down Expand Up @@ -316,6 +334,30 @@ def _refresh_devices(self):
if light is not None:
light.update_resources(props)

def _refresh_locations(self):
"""Retrieve location list from the backend
"""
self.debug("_refresh_locations")
self._locations = []

elocation_data = self._be.get(LOCATIONS_EMERGENCY_PATH)
if elocation_data:
self.debug("got something")
else:
self.debug("got nothing")

url = LOCATIONS_PATH_FORMAT.format(self.be.user_id)
location_data = self._be.get(url)
if not location_data:
self.warning("No locations returned from " + url)
else:
for user_location in location_data.get("userLocations", []):
self._locations.append(ArloLocation(self, user_location))
for shared_location in location_data.get("sharedLocations", []):
self._locations.append(ArloLocation(self, shared_location))

self.vdebug("locations={}".format(pprint.pformat(self._locations)))

def _refresh_camera_thumbnails(self, wait=False):
"""Request latest camera thumbnails, called at start up."""
for camera in self._cameras:
Expand Down Expand Up @@ -381,6 +423,9 @@ def _refresh_modes(self):
for base in self._bases:
base.update_modes()
base.update_mode()
for location in self._locations:
location.update_modes()
location.update_mode()

def _fast_refresh(self):
self.vdebug("aarlo: fast refresh")
Expand Down Expand Up @@ -533,6 +578,19 @@ def base_stations(self):
"""
return self._bases

@property
def locations(self):
"""List of locations..
:return: a list of locations.
:rtype: list(ArloLocation)
"""
return self._locations

@property
def sensors(self):
return self._sensors

@property
def blank_image(self):
"""Return a binaryy representation of a blank image.
Expand Down
65 changes: 41 additions & 24 deletions pyaarlo/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class ArloBackEnd(object):

_session_lock = threading.Lock()
_session_info = {}
_multi_location = False

def __init__(self, arlo):

Expand Down Expand Up @@ -130,6 +131,14 @@ def _save_session(self):
except Exception as e:
self._arlo.warning("session file not written" + str(e))

def _transaction_id(self):
return 'FE!' + str(uuid.uuid4())

def _build_url(self, url, tid):
sep = "&" if "?" in url else "?"
now = time_to_arlotime()
return f"{url}{sep}eventId={tid}&time={now}"

def _request(
self,
path,
Expand All @@ -151,12 +160,12 @@ def _request(
with self._req_lock:
if host is None:
host = self._arlo.cfg.host
url = self._add_extra_params(host + path)
tid = self._transaction_id()
url = self._build_url(host + path, tid)
headers['x-transaction-id'] = tid
self.vdebug("request-url={}".format(url))
self.vdebug("request-params=\n{}".format(pprint.pformat(params)))
self.vdebug(
"request-headers=\n{}".format(pprint.pformat(headers))
)
self.vdebug("request-headers=\n{}".format(pprint.pformat(headers)))
if method == "GET":
r = self._session.get(
url,
Expand Down Expand Up @@ -184,6 +193,7 @@ def _request(
self.vdebug("request-body=\n{}".format(pprint.pformat(body)))
except Exception as e:
self._arlo.warning("body-error={}".format(type(e).__name__))
self._arlo.debug(f"request-text={r.text}")
return None

self.vdebug("request-end={}".format(r.status_code))
Expand Down Expand Up @@ -215,15 +225,6 @@ def _request(
def gen_trans_id(self, trans_type=TRANSID_PREFIX):
return trans_type + "!" + str(uuid.uuid4())

def _add_extra_params(self, url):
if '?' in url:
url = url + '&'
else:
url = url + '?'
eid = str(uuid.uuid4())
now = time_to_arlotime()
return f"{url}event_id=FE!{eid}&time={now}"

def _event_dispatcher(self, response):

# get message type(s) and id(s)
Expand Down Expand Up @@ -304,20 +305,17 @@ def _event_dispatcher(self, response):
# This a list ditch effort to funnel the answer the correct place...
# Check for device_id
# Check for unique_id
# Check for locationId
# If none of those then is unhandled
# Packet number #?.
else:
device_id = response.get("deviceId", None)
device_id = response.get("deviceId",
response.get("uniqueId",
response.get("locationId")))
if device_id is not None:
responses.append((device_id, resource, response))
else:
device_id = response.get("uniqueId", None)
if device_id is not None:
responses.append((device_id, resource, response))
else:
self.debug(
"unhandled response {} - {}".format(resource, response)
)
self.debug(f"unhandled response {resource} - {response}")

# Now find something waiting for this/these.
for device_id, resource, response in responses:
Expand Down Expand Up @@ -652,11 +650,15 @@ def _update_auth_info(self, body):
def _auth(self):
headers = {
"Accept": "application/json, text/plain, */*",
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "en-GB,en;q=0.9,en-US;q=0.8",
"Origin": ORIGIN_HOST,
"Referer": REFERER_HOST,
"Source": "arloCamWeb",
"User-Agent": self._user_agent,
"x-user-device-id": self._user_id,
"x-user-device-name": "QlJPV1NFUg==",
"x-user-device-type": "BROWSER",
}

# Handle 1015 error
Expand Down Expand Up @@ -798,12 +800,16 @@ def _auth(self):
def _validate(self):
headers = {
"Accept": "application/json, text/plain, */*",
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "en-GB,en;q=0.9,en-US;q=0.8",
"Authorization": self._token64,
"Origin": ORIGIN_HOST,
"Referer": REFERER_HOST,
"User-Agent": self._user_agent,
"Source": "arloCamWeb",
"x-user-device-id": self._user_id,
"x-user-device-name": "QlJPV1NFUg==",
"x-user-device-type": "BROWSER",
}

# Validate it!
Expand All @@ -820,6 +826,8 @@ def _v2_session(self):
if v2_session is None:
self._arlo.error("session start failed")
return False
self._multi_location = v2_session.get('supportsMultiLocation', False)
self._arlo.debug(f"multilocation is {self._multi_location}")
return True

def _login(self):
Expand Down Expand Up @@ -847,7 +855,8 @@ def _login(self):
# update sessions headers
headers = {
"Accept": "application/json",
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "en-GB,en;q=0.9,en-US;q=0.8",
"Auth-Version": "2",
"Authorization": self._token,
"Content-Type": "application/json; charset=utf-8;",
Expand Down Expand Up @@ -1066,6 +1075,14 @@ def session(self):
def sub_id(self):
return self._sub_id

@property
def user_id(self):
return self._user_id

@property
def multi_location(self):
return self._multi_location

def add_listener(self, device, callback):
with self._lock:
if device.device_id not in self._callbacks:
Expand Down
Loading

0 comments on commit e365ece

Please sign in to comment.