Skip to content

Commit

Permalink
Merge pull request #9 from natekspencer/dev
Browse files Browse the repository at this point in the history
Group smart water sensor devices with parent device
  • Loading branch information
natekspencer authored Oct 29, 2021
2 parents 192ded3 + 63435eb commit 9a2eb97
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 49 deletions.
2 changes: 1 addition & 1 deletion vivintpy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
"""Provide a package for vivintpy."""
__version__ = "2021.10.0"
__version__ = "2021.10.1"
2 changes: 2 additions & 0 deletions vivintpy/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ class VivintDeviceAttribute:
CAPABILITY_CATEGORY = "caca"
CURRENT_SOFTWARE_VERSION = "csv"
FIRMWARE_VERSION = "fwv"
HIDDEN = "hidden"
ID = "_id"
LOW_BATTERY = "lb"
NAME = "n"
Expand All @@ -108,6 +109,7 @@ class AlarmPanelAttribute(VivintDeviceAttribute):

DEVICES = "d"
PARTITION_ID = "parid"
UNREGISTERED = "ureg"


class CameraAttribute(VivintDeviceAttribute):
Expand Down
32 changes: 26 additions & 6 deletions vivintpy/devices/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from ..const import VivintDeviceAttribute as Attribute
from ..entity import Entity
from ..enums import CapabilityCategoryType, CapabilityType, ZoneBypass
from ..enums import CapabilityCategoryType, CapabilityType, DeviceType, ZoneBypass
from ..vivintskyapi import VivintSkyApi
from ..zjs_device_config_db import get_zwave_device_info

Expand All @@ -17,7 +17,6 @@

def get_device_class(device_type: str) -> Type[VivintDevice]:
"""Map a device_type string to the class that implements that device."""
from ..enums import DeviceType
from . import UnknownDevice
from .camera import Camera
from .door_lock import DoorLock
Expand All @@ -33,6 +32,7 @@ def get_device_class(device_type: str) -> Type[VivintDevice]:
DeviceType.GARAGE_DOOR: GarageDoor,
DeviceType.MULTILEVEL_SWITCH: MultilevelSwitch,
DeviceType.THERMOSTAT: Thermostat,
DeviceType.TOUCH_PANEL: VivintDevice,
DeviceType.WIRELESS_SENSOR: WirelessSensor,
}

Expand All @@ -59,6 +59,7 @@ def __init__(self, data: dict, alarm_panel: AlarmPanel = None) -> None:
if data.get(Attribute.CAPABILITY_CATEGORY)
else None
)
self._parent: VivintDevice | None = None

def __repr__(self) -> str:
"""Return custom __repr__ of device."""
Expand All @@ -69,6 +70,11 @@ def id(self) -> int:
"""Device's id."""
return self.data[Attribute.ID]

@property
def is_valid(self) -> bool:
"""Return `True` if the device is valid."""
return True

@property
def name(self) -> str | None:
"""Device's name."""
Expand All @@ -81,6 +87,16 @@ def capabilities(
"""Device capabilities."""
return self._capabilities

@property
def device_type(self) -> DeviceType:
"""Return the device type."""
return DeviceType(self.data[Attribute.TYPE])

@property
def is_subdevice(self) -> bool:
"""Return if this device is a subdevice."""
return self._parent is not None

@property
def manufacturer(self) -> str | None:
"""Return the manufacturer for this device."""
Expand All @@ -100,13 +116,17 @@ def panel_id(self) -> int:
"""Return the id of the panel this device is associated to."""
return self.data.get(Attribute.PANEL_ID)

@property
def parent(self) -> VivintDevice | None:
"""Return the parent device, if any."""
return self._parent

@property
def serial_number(self) -> str | None:
"""Return the serial number for this device."""
serial_number = self.data.get(Attribute.SERIAL_NUMBER_32_BIT)
serial_number = (
serial_number if serial_number else self.data.get(Attribute.SERIAL_NUMBER)
)
if not serial_number:
serial_number = self.data.get(Attribute.SERIAL_NUMBER)
return serial_number

@property
Expand Down Expand Up @@ -190,4 +210,4 @@ class UnknownDevice(VivintDevice):

def __repr__(self) -> str:
"""Return custom __repr__ of device."""
return f"<{self.__class__.__name__}|{self.data[Attribute.TYPE]} {self.id}>"
return f"<{self.__class__.__name__}|{self.data[Attribute.TYPE]} {self.id}, {self.name}>"
98 changes: 59 additions & 39 deletions vivintpy/devices/alarm_panel.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
"""Module that implements the AlarmPanel class."""
from __future__ import annotations

import asyncio
import logging
from typing import TYPE_CHECKING, Type

from ..const import (
AlarmPanelAttribute,
PubNubMessageAttribute,
PubNubOperatorAttribute,
SystemAttribute,
)
from typing import TYPE_CHECKING, Any, Type

from ..const import AlarmPanelAttribute as Attribute
from ..const import PubNubMessageAttribute, PubNubOperatorAttribute, SystemAttribute
from ..enums import ArmedState, DeviceType
from ..exceptions import VivintSkyApiError
from ..utils import add_async_job, first_or_none
Expand All @@ -32,18 +29,16 @@ def __init__(self, data: dict, system: System):
"""Initialize an alarm panel."""
self.system = system
super().__init__(data)
self.__panel_credentials = None
self.devices: list[VivintDevice] = []
self.unregistered_devices: dict[int, tuple] = {}

# initialize devices
self.devices = [
get_device_class(device_data[AlarmPanelAttribute.TYPE])(device_data, self)
for device_data in self.data[AlarmPanelAttribute.DEVICES]
]
self.__parse_data(data=data, init=True)

# store a reference to the physical panel device
self.__panel_credentials = None
self.__panel = first_or_none(
self.devices,
lambda device: DeviceType(device.data.get(AlarmPanelAttribute.TYPE))
lambda device: DeviceType(device.data.get(Attribute.TYPE))
== DeviceType.TOUCH_PANEL,
)

Expand All @@ -55,7 +50,7 @@ def vivintskyapi(self) -> VivintSkyApi:
@property
def id(self) -> int:
"""Panel's id."""
return self.data[AlarmPanelAttribute.PANEL_ID]
return self.data[Attribute.PANEL_ID]

@property
def name(self) -> str:
Expand Down Expand Up @@ -84,7 +79,7 @@ def software_version(self) -> str:
@property
def partition_id(self) -> int:
"""Panel's partition id."""
return self.data[AlarmPanelAttribute.PARTITION_ID]
return self.data[Attribute.PARTITION_ID]

@property
def is_disarmed(self) -> bool:
Expand All @@ -103,7 +98,7 @@ def is_armed_stay(self) -> bool:

def get_armed_state(self):
"""Return the panel's arm state."""
return self.data[AlarmPanelAttribute.STATE]
return self.data[Attribute.STATE]

async def set_armed_state(self, state: int) -> None:
"""Set the armed state for a panel."""
Expand Down Expand Up @@ -146,29 +141,14 @@ def get_devices(

return devices

def refresh(self, data: dict, new_device: bool = False) -> None:
def refresh(self, data: dict[str, Any], new_device: bool = False) -> None:
"""Refresh the alarm panel."""
if not new_device:
self.update_data(data, override=True)
else:
self.data[AlarmPanelAttribute.DEVICES].extend(
data[AlarmPanelAttribute.DEVICES]
)
self.data[Attribute.DEVICES].extend(data[Attribute.DEVICES])

# update associated devices
for device_data in data[AlarmPanelAttribute.DEVICES]:
device = first_or_none(
self.devices,
lambda device: device.id == device_data[AlarmPanelAttribute.ID],
)
if device:
device.update_data(device_data, override=True)
else:
device = get_device_class(device_data[AlarmPanelAttribute.TYPE])(
device_data, self
)
self.devices.append(device)
self.emit(DEVICE_DISCOVERED, device_data)
self.__parse_data(data)

def handle_pubnub_message(self, message: dict) -> None:
"""Handle a pubnub message."""
Expand Down Expand Up @@ -197,6 +177,7 @@ def handle_pubnub_message(self, message: dict) -> None:
continue

if operation == PubNubOperatorAttribute.CREATE:
self.refresh(data=data, new_device=True)
add_async_job(self.handle_new_device, device_id)
else:
device = first_or_none(
Expand All @@ -211,24 +192,63 @@ def handle_pubnub_message(self, message: dict) -> None:

# for the sake of consistency, we also need to update the panel's raw data
raw_device_data = first_or_none(
self.data[AlarmPanelAttribute.DEVICES],
self.data[Attribute.DEVICES],
lambda raw_device_data: raw_device_data["_id"]
== device_data["_id"],
)

if operation == PubNubOperatorAttribute.DELETE:
self.devices.remove(device)
self.data[AlarmPanelAttribute.DEVICES].remove(raw_device_data)
self.emit(DEVICE_DELETED, raw_device_data)
self.data[Attribute.DEVICES].remove(raw_device_data)
self.unregistered_devices[device.id] = (
device.name,
device.device_type,
)
self.emit(DEVICE_DELETED, {"device": device})
else:
device.handle_pubnub_message(device_data)
raw_device_data.update(device_data)

async def handle_new_device(self, device_id: int) -> None:
"""Handle a new device."""
try:
device = first_or_none(self.devices, lambda device: device.id == device_id)
while not device.is_valid:
await asyncio.sleep(1)
if device.id in self.unregistered_devices:
return
resp = await self.vivintskyapi.get_device_data(self.id, device_id)
data = resp[SystemAttribute.SYSTEM][SystemAttribute.PARTITION][0]
self.refresh(data, new_device=True)
self.emit(DEVICE_DISCOVERED, {"device": device})
except VivintSkyApiError:
_LOGGER.error("Error getting new device data for device %s", device_id)

def __parse_data(self, data: dict[str, Any], init: bool = False) -> None:
"""Parse the alarm panel data."""
for device_data in data[Attribute.DEVICES]:
device: VivintDevice = None
if not init:
device = first_or_none(
self.devices,
lambda device: device.id == device_data[Attribute.ID],
)
if device:
device.update_data(device_data, override=True)
else:
self.__parse_device_data(device_data=device_data)

if data.get(Attribute.UNREGISTERED):
self.unregistered_devices: dict[int, tuple] = {
device[Attribute.ID]: (
device[Attribute.NAME],
DeviceType(device[Attribute.TYPE]),
)
for device in data[Attribute.UNREGISTERED]
}

def __parse_device_data(self, device_data: dict[str, Any]) -> None:
"""Parse device data and optionally emit a device discovered event."""
device_class = get_device_class(device_data[Attribute.TYPE])
device = device_class(device_data, self)
self.devices.append(device)
29 changes: 29 additions & 0 deletions vivintpy/devices/wireless_sensor.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
"""Module that implements the WirelessSensor class."""
import logging
from typing import Any

from ..const import WirelessSensorAttribute as Attributes
from ..enums import EquipmentCode, EquipmentType, SensorType
from ..utils import first_or_none
from . import BypassTamperDevice, VivintDevice
from .alarm_panel import AlarmPanel

_LOGGER = logging.getLogger(__name__)


class WirelessSensor(BypassTamperDevice, VivintDevice):
"""Represents a Vivint wireless sensor device."""

def __init__(self, data: dict, alarm_panel: AlarmPanel = None) -> None:
"""Initialize a wireless sesnor."""
super().__init__(data=data, alarm_panel=alarm_panel)
self.__update_parent()

def __repr__(self) -> str:
"""Return custom __repr__ of wireless sensor."""
return (
Expand Down Expand Up @@ -64,6 +72,27 @@ def low_battery(self) -> bool:
"""Return true if battery's level is low."""
return self.data.get(Attributes.LOW_BATTERY, False)

@property
def is_valid(self) -> bool:
"""Return `True` if the wireless sensor is valid."""
return (
self.serial_number is not None
and self.equipment_code != EquipmentCode.OTHER
and self.sensor_type != SensorType.UNUSED
)

def update_data(self, new_val: dict[str, Any], override: bool = False) -> None:
"""Update entity's raw data."""
super().update_data(new_val=new_val, override=override)
self.__update_parent()

def __update_parent(self) -> None:
if self.data.get(Attributes.HIDDEN) and self._parent is None:
self._parent = first_or_none(
self.alarm_panel.devices,
lambda parent: parent.serial_number == self.serial_number,
)

async def set_bypass(self, bypass: bool) -> None:
"""Bypass/unbypass the sensor."""
await self.vivintskyapi.set_sensor_state(
Expand Down
5 changes: 4 additions & 1 deletion vivintpy/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,7 @@ def unsubscribe() -> None:
def emit(self, event_name: str, data: dict) -> None:
"""Run all callbacks for an event."""
for listener in self._listeners.get(event_name, []):
listener(data)
try:
listener(data)
except: # noqa E722
pass
10 changes: 8 additions & 2 deletions vivintpy/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,12 @@ class EquipmentCode(IntEnum):
VS_CO3_DETECTOR = 1266
VS_SMKT_SMOKE_DETECTOR = 1267

# Handle unknown/future equipment codes
UNKNOWN = -1

@classmethod
def _missing_(cls, value):
return cls.OTHER
return cls.UNKNOWN


@unique
Expand Down Expand Up @@ -328,9 +331,12 @@ class SensorType(IntEnum):
SILENT_BURGLARY = 24
UNUSED = 0

# Handle unknown/future sensor types.
UNKNOWN = -1

@classmethod
def _missing_(cls, value):
return cls.UNUSED
return cls.UNKNOWN


@unique
Expand Down

0 comments on commit 9a2eb97

Please sign in to comment.