"""Aqara camera support.
Support for lumi.camera.aq1
.. todo:: add alarm/sound parts (get_music_info, {get,set}_alarming_volume,
set_default_music, play_music_new, set_sound_playing)
.. todo:: add sdcard status & fix all TODOS
.. todo:: add tests
"""
import logging
from enum import IntEnum
from typing import Any, Dict
import attr
import click
from miio import Device, DeviceStatus
from miio.click_common import command, format_output
_LOGGER = logging.getLogger(__name__)
[docs]
@attr.s
class CameraOffset:
"""Container for camera offset data."""
x = attr.ib()
y = attr.ib()
radius = attr.ib()
[docs]
@attr.s
class ArmStatus:
"""Container for arm statuses."""
is_armed: bool = attr.ib(converter=bool)
arm_wait_time: int = attr.ib(converter=int)
alarm_volume: int = attr.ib(converter=int)
[docs]
class SDCardStatus(IntEnum):
"""State of the SD card."""
NoCardInserted = 0
Ok = 1
FormatRequired = 2
Formating = 3
[docs]
class MotionDetectionSensitivity(IntEnum):
"""'Default' values for md sensitivity.
Currently unused as the value can also be set arbitrarily.
"""
High = 6000000
Medium = 10000000
Low = 11000000
[docs]
class CameraStatus(DeviceStatus):
"""Container for status reports from the Aqara Camera."""
def __init__(self, data: Dict[str, Any]) -> None:
"""Response of a lumi.camera.aq1:
{"p2p_id":"#################","app_type":"celing",
"offset_x":"0","offset_y":"0","offset_radius":"0",
"md_status":1,"video_state":1,"fullstop":0,
"led_status":1,"ir_status":1,"mdsensitivity":6000000,
"channel_id":0,"flip_state":0,
"avID":"####","avPass":"####","id":65001}
"""
self.data = data
@property
def type(self) -> str:
"""TODO: Type of the camera? Name?"""
return self.data["app_type"]
@property
def video_status(self) -> bool:
"""Video state."""
return bool(self.data["video_state"])
@property
def is_on(self) -> bool:
"""True if device is currently on."""
return self.video_status == 1
@property
def md(self) -> bool:
"""Motion detection state."""
return bool(self.data["md_status"])
@property
def md_sensitivity(self):
"""Motion detection sensitivity."""
return self.data["mdsensitivity"]
@property
def ir(self):
"""IR mode."""
return bool(self.data["ir_status"])
@property
def led(self):
"""LED status."""
return bool(self.data["led_status"])
@property
def flipped(self) -> bool:
"""TODO: If camera is flipped?"""
return self.data["flip_state"]
@property
def offsets(self) -> CameraOffset:
"""Camera offset information."""
return CameraOffset(
x=self.data["offset_x"],
y=self.data["offset_y"],
radius=self.data["offset_radius"],
)
@property
def channel_id(self) -> int:
"""TODO: Zigbee channel?"""
return self.data["channel_id"]
@property
def fullstop(self) -> bool:
"""Is alarm triggered by MD."""
return self.data["fullstop"] != 0
@property
def p2p_id(self) -> str:
"""P2P ID for video and audio."""
return self.data["p2p_id"]
@property
def av_id(self) -> str:
"""TODO: What is this? ID for the cloud?"""
return self.data["avID"]
@property
def av_password(self) -> str:
"""TODO: What is this? Password for the cloud?"""
return self.data["avPass"]
[docs]
class AqaraCamera(Device):
"""Main class representing the Xiaomi Aqara Camera."""
_supported_models = ["lumi.camera.aq1", "lumi.camera.aq2"]
[docs]
@command(
default_output=format_output(
"",
"Type: {result.type}\n"
"Video: {result.is_on}\n"
"Offsets: {result.offsets}\n"
"IR: {result.ir_status} %\n"
"MD: {result.md_status} (sensitivity: {result.md_sensitivity}\n"
"LED: {result.led}\n"
"Flipped: {result.flipped}\n"
"Full stop: {result.fullstop}\n"
"P2P ID: {result.p2p_id}\n"
"AV ID: {result.av_id}\n"
"AV password: {result.av_password}\n"
"\n",
)
)
def status(self) -> CameraStatus:
"""Camera status."""
return CameraStatus(self.send("get_ipcprop", ["all"]))
[docs]
@command(default_output=format_output("Camera on"))
def on(self):
"""Camera on."""
return self.send("set_video", ["on"])
[docs]
@command(default_output=format_output("Camera off"))
def off(self):
"""Camera off."""
return self.send("set_video", ["off"])
[docs]
@command(default_output=format_output("IR on"))
def ir_on(self):
"""IR on."""
return self.send("set_ir", ["on"])
[docs]
@command(default_output=format_output("IR off"))
def ir_off(self):
"""IR off."""
return self.send("set_ir", ["off"])
[docs]
@command(default_output=format_output("MD on"))
def md_on(self):
"""IR on."""
return self.send("set_md", ["on"])
[docs]
@command(default_output=format_output("MD off"))
def md_off(self):
"""MD off."""
return self.send("set_md", ["off"])
[docs]
@command(click.argument("sensitivity", type=int, required=False))
def md_sensitivity(self, sensitivity):
"""Get or set the motion detection sensitivity."""
if sensitivity:
click.echo("Setting MD sensitivity to %s" % sensitivity)
return self.send("set_mdsensitivity", [sensitivity])[0] == "ok"
else:
return self.send("get_mdsensitivity")
[docs]
@command(default_output=format_output("LED on"))
def led_on(self):
"""LED on."""
return self.send("set_led", ["on"])
[docs]
@command(default_output=format_output("LED off"))
def led_off(self):
"""LED off."""
return self.send("set_led", ["off"])
[docs]
@command(default_output=format_output("Flip on"))
def flip_on(self):
"""Flip on."""
return self.send("set_flip", ["on"])
[docs]
@command(default_output=format_output("Flip off"))
def flip_off(self):
"""Flip off."""
return self.send("set_flip", ["off"])
[docs]
@command(default_output=format_output("Fullstop on"))
def fullstop_on(self):
"""Fullstop on."""
return self.send("set_fullstop", ["on"])
[docs]
@command(default_output=format_output("Fullstop off"))
def fullstop_off(self):
"""Fullstop off."""
return self.send("set_fullstop", ["off"])
[docs]
@command(
click.argument("time", type=int, default=30),
default_output=format_output("Start pairing for {time} seconds"),
)
def pair(self, timeout: int):
"""Start (or stop with "0") pairing."""
if timeout < 0:
raise ValueError("Invalid timeout: %s" % timeout)
return self.send("start_zigbee_join", [timeout])
[docs]
@command()
def sd_status(self):
"""SD card status."""
return SDCardStatus(self.send("get_sdstatus"))
[docs]
@command()
def arm_status(self):
"""Return arming information."""
is_armed = self.send("get_arming")
arm_wait_time = self.send("get_arm_wait_time")
alarm_volume = self.send("get_alarming_volume")
return ArmStatus(
is_armed=bool(is_armed),
arm_wait_time=arm_wait_time,
alarm_volume=alarm_volume,
)
[docs]
@command(
click.argument("volume", type=int, default=100),
default_output=format_output("Setting alarm volume to {volume}"),
)
def set_alarm_volume(self, volume):
"""Set alarm volume."""
if volume < 0 or volume > 100:
raise ValueError("Volume has to be [0,100], was %s" % volume)
return self.send("set_alarming_volume", [volume])[0] == "ok"
[docs]
@command(click.argument("sound_id", type=str, required=False, default=None))
def alarm_sound(self, sound_id):
"""List or set the alarm sound."""
if id is None:
sound_status = self.send("get_music_info", [0])
# TODO: make a list out from this.
@attr.s
class SoundList:
default = attr.ib()
total = attr.ib(type=int)
sounds = attr.ib(type=list)
return sound_status
click.echo("Setting alarm sound to %s" % sound_id)
return self.send("set_default_music", [0, sound_id])[0] == "ok"
[docs]
@command(default_output=format_output("Arming"))
def arm(self):
"""Arm the camera?"""
return self.send("set_arming", ["on"])
[docs]
@command(default_output=format_output("Disarming"))
def disarm(self):
"""Disarm the camera?"""
return self.send("set_arming", ["off"])