1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381
|
from typing import TYPE_CHECKING
from unidecode import unidecode
if TYPE_CHECKING:
from . import PyArlo
from .constant import (
BATTERY_KEY,
BATTERY_TECH_KEY,
CHARGER_KEY,
CHARGING_KEY,
CONNECTION_KEY,
CONNECTIVITY_KEY,
DEVICE_KEYS,
RESOURCE_KEYS,
RESOURCE_UPDATE_KEYS,
SIGNAL_STR_KEY,
TIMEZONE_KEY,
XCLOUD_ID_KEY,
)
from .super import ArloSuper
class ArloDevice(ArloSuper):
"""Base class for all Arlo devices.
Has code to handle providing common attributes and comment event handling.
"""
def __init__(self, name, arlo: 'PyArlo', attrs):
super().__init__(name, arlo, attrs,
id=attrs.get("deviceId", "unknown"),
type=attrs.get("deviceType", "unknown"),
uid=attrs.get("uniqueId", None))
# We save this here but only expose it directly in the ArloChild class.
# Some devices are their own parents and we need to know that at the ArloDevice
# or ArloChild class so we leave this here as a short cut.
self._parent_id = attrs.get("parentId", None)
# Activities. Used by camera for now but made available to all.
self._activities = {}
# Build initial values. These can be at the top level or in the
# properties dictionary.
for key in DEVICE_KEYS:
value = attrs.get(key, None)
if value is not None:
self._save(key, value)
props = attrs.get("properties", {})
for key in RESOURCE_KEYS + RESOURCE_UPDATE_KEYS:
value = props.get(key, None)
if value is not None:
self._save(key, value)
@property
def entity_id(self):
if self._arlo.cfg.serial_ids:
return self.device_id
elif self._arlo.cfg.no_unicode_squash:
return self.name.lower().replace(" ", "_")
else:
return unidecode(self.name.lower().replace(" ", "_"))
@property
def resource_id(self):
"""Returns the resource id, used for making requests and checking responses.
For base stations has the format [DEVICE-ID] and for other devices has
the format [RESOURCE-TYPE]/[DEVICE-ID]
"""
return self.device_id
@property
def resource_type(self):
"""Returns the type of resource this is.
For now it's, `cameras`, `doorbells`, `lights` or `basestations`.
"""
return None
@property
def serial_number(self):
"""Returns the device serial number."""
return self.device_id
@property
def model_id(self):
"""Returns the model id."""
return self._attrs.get("modelId", None)
@property
def hw_version(self):
"""Returns the hardware version."""
return self._attrs.get("properties", {}).get("hwVersion", None)
@property
def timezone(self):
"""Returns the timezone."""
time_zone = self._load(TIMEZONE_KEY, None)
if time_zone is None:
return self._attrs.get("properties", {}).get("olsonTimeZone", None)
return time_zone
@property
def user_id(self):
"""Returns the user id."""
return self._attrs.get("userId", None)
@property
def user_role(self):
"""Returns the user role."""
return self._attrs.get("userRole", None)
@property
def xcloud_id(self):
"""Returns the device's xcloud id."""
return self._load(XCLOUD_ID_KEY, "UNKNOWN")
@property
def web_id(self):
"""Return the device's web id."""
return self.user_id + "_web"
@property
def is_own_parent(self):
"""Returns True if device is its own parent.
Can work from child or parent class.
"""
return self._parent_id == self.device_id
def attribute(self, attr, default=None):
"""Return the value of attribute attr.
PyArlo stores its state in key/value pairs. This returns the value associated with the key.
See PyArlo for a non-exhaustive list of attributes.
:param attr: Attribute to look up.
:type attr: str
:param default: value to return if not found.
:return: The value associated with attribute or `default` if not found.
"""
value = self._load(attr, None)
if value is None:
value = self._attrs.get(attr, None)
if value is None:
value = self._attrs.get("properties", {}).get(attr, None)
if value is None:
value = default
return value
def add_attr_callback(self, attr, cb):
"""Add an callback to be triggered when an attribute changes.
Used to register callbacks to track device activity. For example, get a notification whenever
motion stop and starts.
See PyArlo for a non-exhaustive list of attributes.
:param attr: Attribute - eg `motionStarted` - to monitor.
:type attr: str
:param cb: Callback to run.
"""
with self._lock:
self._attr_cbs_.append((attr, cb))
def has_capability(self, cap):
"""Is the device capable of performing activity cap:.
Used to determine if devices can perform certain actions, like motion or audio detection.
See attribute list against PyArlo.
:param cap: Attribute - eg `motionStarted` - to check.
:return: `True` it is, `False` it isn't.
"""
if cap in (CONNECTION_KEY,):
return True
return False
@property
def state(self):
"""Returns a string describing the device's current state."""
return "idle"
@property
def is_on(self):
"""Returns `True` if the device is on, `False` otherwise."""
return True
def turn_on(self):
"""Turn the device on."""
pass
def turn_off(self):
"""Turn the device off."""
pass
@property
def is_unavailable(self):
"""Returns `True` if the device is unavailable, `False` otherwise.
**Note:** Sorry about the double negative.
"""
return self._load(CONNECTION_KEY, "unknown") == "unavailable"
@property
def battery_level(self):
"""Returns the current battery level."""
return self._load(BATTERY_KEY, 100)
@property
def battery_tech(self):
"""Returns the current battery technology.
Is it rechargable, wired...
"""
return self._load(BATTERY_TECH_KEY, "None")
@property
def has_batteries(self):
"""Returns `True` if device has batteries installed, `False` otherwise."""
return self.battery_tech != "None"
@property
def charger_type(self):
"""Returns how the device is recharging."""
return self._load(CHARGER_KEY, "None")
@property
def has_charger(self):
"""Returns `True` if the charger is plugged in, `False` otherwise."""
return self.charger_type != "None"
@property
def is_charging(self):
"""Returns `True` if the device is charging, `False` otherwise."""
return self._load(CHARGING_KEY, "off").lower() == "on"
@property
def is_charger_only(self):
"""Returns `True` if the cahrger is plugged in with no batteries, `False` otherwise."""
return self.battery_tech == "None" and self.has_charger
@property
def is_corded(self):
"""Returns `True` if the device is connected directly to a power outlet, `False` otherwise.
The device can't have any battery option, it can't be using a charger, it has to run
directly from a plug. ie, an original base station.
"""
return not self.has_batteries and not self.has_charger
@property
def using_wifi(self):
"""Returns `True` if the device is connected to the wifi, `False` otherwise.
This means connecting directly to your home wifi, not connecting to and Arlo basestation.
"""
return self._attrs.get(CONNECTIVITY_KEY, {}).get("type", "").lower() == "wifi"
@property
def signal_strength(self):
"""Returns the WiFi signal strength (0-5)."""
return self._load(SIGNAL_STR_KEY, 3)
def debug(self, msg):
self._arlo.debug(f"{self.device_id}: {msg}")
def vdebug(self, msg):
self._arlo.vdebug(f"{self.device_id}: {msg}")
class ArloChildDevice(ArloDevice):
"""Base class for all Arlo devices that attach to a base station."""
def __init__(self, name, arlo, attrs):
super().__init__(name, arlo, attrs)
self.debug("parent is {}".format(self._parent_id))
self.vdebug("resource is {}".format(self.resource_id))
def _event_handler(self, resource, event):
self.vdebug("{}: child got {} event **".format(self.name, resource))
if resource.endswith("/states"):
self._arlo.bg.run(self.base_station.update_mode)
return
# Pass event to lower level.
super()._event_handler(resource, event)
@property
def resource_type(self):
"""Return the resource type this child device describes.
Currently limited to `camera`, `doorbell` and `light`.
"""
return "child"
@property
def resource_id(self):
"""Returns the child device resource id.
Some devices - certain cameras - can provide other types.
"""
return self.resource_type + "/" + self.device_id
@property
def parent_id(self):
"""Returns the parent device id.
**Note:** Some devices - ArloBaby for example - are their own parents.
"""
if self._parent_id is not None:
return self._parent_id
return self.device_id
@property
def timezone(self):
"""Returns the timezone.
Tries to be clever. If it doesn't have a timezone it will try its
basestation.
"""
time_zone = super().timezone
if time_zone is None:
return self.base_station.timezone
return time_zone
@property
def base_station(self):
"""Returns the base station controlling this device.
Some devices - ArloBaby for example - are their own parents. If we
can't find a basestation, this returns the first one (if any exist).
"""
# look for real parents
for base in self._arlo.base_stations:
if base.device_id == self.parent_id:
return base
# some cameras don't have base stations... it's its own base station...
for base in self._arlo.base_stations:
if base.device_id == self.device_id:
return base
# no idea!
if len(self._arlo.base_stations) > 0:
return self._arlo.base_stations[0]
self._arlo.error("Could not find any base stations for device " + self._name)
return None
@property
def is_unavailable(self):
if not self.base_station:
return True
return (
self.base_station.is_unavailable
or self._load(CONNECTION_KEY, "unknown") == "unavailable"
)
@property
def too_cold(self):
"""Returns `True` if the device too cold to operate, `False` otherwise."""
return self._load(CONNECTION_KEY, "unknown") == "thermalShutdownCold"
@property
def state(self):
if self.is_unavailable:
return "unavailable"
if not self.is_on:
return "off"
if self.too_cold:
return "offline, too cold"
return "idle"
|