From 714e5f63f9043e7da7d4fd2b37fe1549f8f46f27 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Sat, 1 Sep 2018 22:33:54 +0300 Subject: [PATCH 1/6] Add initial support for aqara camera --- miio/__init__.py | 1 + miio/aqaracamera.py | 291 ++++++++++++++++++++++++++++++++++++++++++++ miio/discovery.py | 3 +- 3 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 miio/aqaracamera.py diff --git a/miio/__init__.py b/miio/__init__.py index 1b4247011..14d9f2a6c 100644 --- a/miio/__init__.py +++ b/miio/__init__.py @@ -23,5 +23,6 @@ from miio.wifirepeater import WifiRepeater from miio.wifispeaker import WifiSpeaker from miio.yeelight import Yeelight +from miio.aqaracamera import AqaraCamera from miio.discovery import Discovery diff --git a/miio/aqaracamera.py b/miio/aqaracamera.py new file mode 100644 index 000000000..65031541f --- /dev/null +++ b/miio/aqaracamera.py @@ -0,0 +1,291 @@ +"""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 attr +import logging +from typing import Any, Dict, Tuple + +import click + +from .click_common import command, format_output +from .device import Device, DeviceException + +_LOGGER = logging.getLogger(__name__) + + +class CameraException(DeviceException): + pass + + +@attr.s +class CameraOffset: + """Container for camera offset data.""" + x = attr.ib() + y = attr.ib() + radius = attr.ib() + + +class CameraStatus: + """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: + """TODO what is md? motion detection?""" + return bool(self.data["md_status"]) + + @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: + """TODO: What is this?""" + return bool(self.data["fullstop"]) + + @property + def p2p_id(self) -> str: + """TODO: What is this? Cloud?""" + 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"] + + + def __repr__(self) -> str: + s = "" \ + % (self.is_on, + self.type, + self.offsets, + self.ir, + self.md, + self.led, + self.flipped, + self.fullstop, + ) + return s + + def __json__(self): + return self.data + + +class AqaraCamera(Device): + """Main class representing the Xiaomi Aqara Camera.""" + + @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}\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"])) + + @command( + default_output=format_output("Camera on"), + ) + def on(self): + """Camera on.""" + return self.send("set_video", ["on"]) + + @command( + default_output=format_output("Camera off"), + ) + def off(self): + """Camera off.""" + return self.send("set_video", ["off"]) + + @command( + default_output=format_output("IR on") + ) + def ir_on(self): + """IR on.""" + return self.send("set_ir", ["on"]) + + @command( + default_output=format_output("IR off") + ) + def ir_off(self): + """IR off.""" + return self.send("set_ir", ["off"]) + + @command( + default_output=format_output("MD on") + ) + def md_on(self): + """IR on.""" + return self.send("set_md", ["on"]) + + @command( + default_output=format_output("MD off") + ) + def md_off(self): + """MD off.""" + return self.send("set_md", ["off"]) + + @command( + default_output=format_output("LED on") + ) + def led_on(self): + """LED on.""" + return self.send("set_led", ["on"]) + + @command( + default_output=format_output("LED off") + ) + def led_off(self): + """LED off.""" + return self.send("set_led", ["off"]) + + @command( + default_output=format_output("Flip on") + ) + def flip_on(self): + """Flip on.""" + return self.send("set_flip", ["on"]) + + @command( + default_output=format_output("Flip off") + ) + def flip_off(self): + """Flip off.""" + return self.send("set_flip", ["off"]) + + @command( + default_output=format_output("Fullstop on") + ) + def fullstop_on(self): + """Fullstop on.""" + return self.send("set_fullstop", ["on"]) + + @command( + default_output=format_output("Fullstop off") + ) + def fullstop_off(self): + """Fullstop off.""" + return self.send("set_fullstop", ["off"]) + + @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 CameraException("Invalid timeout: %s" % timeout) + + return self.send("start_zigbee_join", [timeout]) + + @command() + def sd_status(self): + """SD card status. TODO: please report output.""" + return self.send("get_sdstatus") + + @command() + def sd_format(self): + """TODO: Format SD card? parameters & result unknown.""" + return self.send("sdformat") + + @command() + def arm_status(self): + """Return arming information.""" + # TODO: return a container + is_armed = self.send("get_arming") + arm_wait_time = self.send("get_arm_wait_time") + return {'is_armed': is_armed, 'wait_time': arm_wait_time} + + @command( + default_output=format_output("Arming") + ) + def arm(self): + """Arm the camera?""" + return self.send("set_arming", ["on"]) + + @command( + default_output=format_output("Disarming") + ) + def disarm(self): + """Disarm the camera?""" + return self.send("set_arming", ["off"]) \ No newline at end of file diff --git a/miio/discovery.py b/miio/discovery.py index bb9f2ca93..01086efba 100644 --- a/miio/discovery.py +++ b/miio/discovery.py @@ -10,7 +10,7 @@ from . import (Device, Vacuum, ChuangmiPlug, PowerStrip, AirPurifier, AirFresh, Ceil, PhilipsBulb, PhilipsEyecare, PhilipsMoonlight, ChuangmiIr, AirHumidifier, WaterPurifier, WifiSpeaker, WifiRepeater, - Yeelight, Fan, Cooker, AirConditioningCompanion, AirQualityMonitor) + Yeelight, Fan, Cooker, AirConditioningCompanion, AirQualityMonitor, AqaraCamera) from .airconditioningcompanion import (MODEL_ACPARTNER_V1, MODEL_ACPARTNER_V2, MODEL_ACPARTNER_V3, ) from .airhumidifier import (MODEL_HUMIDIFIER_CA1, MODEL_HUMIDIFIER_V1, ) @@ -69,6 +69,7 @@ "lumi-acpartner-v1": partial(AirConditioningCompanion, model=MODEL_ACPARTNER_V1), "lumi-acpartner-v2": partial(AirConditioningCompanion, model=MODEL_ACPARTNER_V2), "lumi-acpartner-v3": partial(AirConditioningCompanion, model=MODEL_ACPARTNER_V3), + "lumi-camera-aq2": AqaraCamera, "yeelink-light-": Yeelight, "zhimi-fan-v2": partial(Fan, model=MODEL_FAN_V2), "zhimi-fan-v3": partial(Fan, model=MODEL_FAN_V3), From a19228b7b4505b5c069a563527f2194042f043fa Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Sat, 1 Sep 2018 22:36:48 +0300 Subject: [PATCH 2/6] Fix linting --- miio/aqaracamera.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/miio/aqaracamera.py b/miio/aqaracamera.py index 65031541f..efd7f1b7d 100644 --- a/miio/aqaracamera.py +++ b/miio/aqaracamera.py @@ -8,7 +8,7 @@ """ import attr import logging -from typing import Any, Dict, Tuple +from typing import Any, Dict import click @@ -113,7 +113,6 @@ def av_password(self) -> str: """TODO: What is this? Password for the cloud?""" return self.data["avPass"] - def __repr__(self) -> str: s = " str: self.md, self.led, self.flipped, - self.fullstop, - ) + self.fullstop + ) return s def __json__(self): @@ -288,4 +287,4 @@ def arm(self): ) def disarm(self): """Disarm the camera?""" - return self.send("set_arming", ["off"]) \ No newline at end of file + return self.send("set_arming", ["off"]) From ea0ca1f62d88619604837e389095dcc1018fa148 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Fri, 7 Sep 2018 16:51:14 +0200 Subject: [PATCH 3/6] Add missing details thanks to feedback from @miguelangel-nubla --- miio/aqaracamera.py | 98 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 87 insertions(+), 11 deletions(-) diff --git a/miio/aqaracamera.py b/miio/aqaracamera.py index efd7f1b7d..53eea2a0c 100644 --- a/miio/aqaracamera.py +++ b/miio/aqaracamera.py @@ -9,6 +9,7 @@ import attr import logging from typing import Any, Dict +from enum import IntEnum import click @@ -30,6 +31,29 @@ class CameraOffset: radius = attr.ib() +@attr.s +class ArmStatus: + """Container for arm statuses.""" + is_armed = attr.ib(converter=bool) + arm_wait_time = attr.ib(converter=int) + alarm_volume = attr.ib(converter=int) + + +class SDCardStatus(IntEnum): + """State of the SD card.""" + NoCardInserted = 0 + Ok = 1 + FormatRequired = 2 + Formating = 3 + +class MotionDetectionSensitivity(IntEnum): + """'Default' values for md sensitivity. + Currently unused as the value can also be set arbitrarily. + """ + High = 6000000 + Medium = 10000000 + Low = 11000000 + class CameraStatus: """Container for status reports from the Aqara Camera.""" @@ -63,9 +87,14 @@ def is_on(self) -> bool: @property def md(self) -> bool: - """TODO what is md? motion detection?""" + """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.""" @@ -95,12 +124,12 @@ def channel_id(self) -> int: @property def fullstop(self) -> bool: - """TODO: What is this?""" - return bool(self.data["fullstop"]) + """Is alarm triggered by MD.""" + return self.data["fullstop"] != 0 @property def p2p_id(self) -> str: - """TODO: What is this? Cloud?""" + """P2P ID for video and audio.""" return self.data["p2p_id"] @property @@ -119,6 +148,7 @@ def __repr__(self) -> str: "offset=%s, " \ "ir=%s, " \ "md=%s, " \ + "md_sensitivity=%s, " \ "led=%s, " \ "flip=%s, " \ "fullstop=%s>" \ @@ -127,6 +157,7 @@ def __repr__(self) -> str: self.offsets, self.ir, self.md, + self.md_sensitivity, self.led, self.flipped, self.fullstop @@ -147,7 +178,7 @@ class AqaraCamera(Device): "Video: {result.is_on}\n" "Offsets: {result.offsets}\n" "IR: {result.ir_status} %\n" - "MD: {result.md_status}\n" + "MD: {result.md_status} (sensitivity: {result.md_sensitivity}\n" "LED: {result.led}\n" "Flipped: {result.flipped}\n" "Full stop: {result.fullstop}\n" @@ -203,6 +234,17 @@ def md_off(self): """MD off.""" return self.send("set_md", ["off"]) + @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") + @command( default_output=format_output("LED on") ) @@ -259,21 +301,55 @@ def pair(self, timeout: int): @command() def sd_status(self): - """SD card status. TODO: please report output.""" - return self.send("get_sdstatus") + """SD card status.""" + return SDCardStatus(self.send("get_sdstatus")) @command() def sd_format(self): - """TODO: Format SD card? parameters & result unknown.""" - return self.send("sdformat") + """Format the SD card. + + Returns True when formating has started successfully. + """ + return bool(self.send("sdformat")) @command() def arm_status(self): """Return arming information.""" - # TODO: return a container is_armed = self.send("get_arming") arm_wait_time = self.send("get_arm_wait_time") - return {'is_armed': is_armed, 'wait_time': 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) + + @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 CameraException("Volume has to be [0,100], was %s" % volume) + return self.send("set_alarming_volume", [volume])[0] == 'ok' + + @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]) + @attr.s + class SoundList: + default = attr.ib() + total = attr.ib(type=int) + sounds = attr.ib(type=list) + + click.echo("Setting alarm sound to %s" % sound_id) + return self.send("set_default_music", [0, sound_id])[0] == 'ok' @command( default_output=format_output("Arming") From e65f8274edcbf4160e42f85de743e5ccb47cca62 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Fri, 7 Sep 2018 16:53:10 +0200 Subject: [PATCH 4/6] add a todo, return list of available audio --- miio/aqaracamera.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/miio/aqaracamera.py b/miio/aqaracamera.py index 53eea2a0c..9e9f1e002 100644 --- a/miio/aqaracamera.py +++ b/miio/aqaracamera.py @@ -342,12 +342,15 @@ 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' From 24b13c87c209423b5ff8116f245045924cc5ed81 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Sat, 8 Dec 2018 21:55:29 +0100 Subject: [PATCH 5/6] fix linting --- miio/aqaracamera.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/miio/aqaracamera.py b/miio/aqaracamera.py index 9e9f1e002..b8a352f9b 100644 --- a/miio/aqaracamera.py +++ b/miio/aqaracamera.py @@ -2,7 +2,8 @@ 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 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 """ @@ -46,6 +47,7 @@ class SDCardStatus(IntEnum): FormatRequired = 2 Formating = 3 + class MotionDetectionSensitivity(IntEnum): """'Default' values for md sensitivity. Currently unused as the value can also be set arbitrarily. @@ -54,6 +56,7 @@ class MotionDetectionSensitivity(IntEnum): Medium = 10000000 Low = 11000000 + class CameraStatus: """Container for status reports from the Aqara Camera.""" @@ -342,6 +345,7 @@ 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: From 8b838a88d6a11a5c68379c508c99586e4036cdef Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Sat, 8 Dec 2018 22:09:46 +0100 Subject: [PATCH 6/6] add aqaracamera to readme and docs --- README.rst | 1 + docs/miio.rst | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/README.rst b/README.rst index d06e98555..820359afc 100644 --- a/README.rst +++ b/README.rst @@ -12,6 +12,7 @@ Supported devices - :doc:`Xiaomi Mi Robot Vacuum ` (:class:`miio.vacuum`) - Xiaomi Mi Home Air Conditioner Companion (:class:`miio.airconditioningcompanion`) - Xiaomi Mi Air Purifier (:class:`miio.airpurifier`) +- Xiaomi Aqara Camera (:class:`miia.aqaracamera`) - :doc:`Xiaomi Mi Smart WiFi Socket ` (:class:`miio.chuangmi_plug`) - :doc:`Xiaomi Chuangmi Plug V1 (1 Socket, 1 USB Port) ` (:class:`miio.chuangmi_plug`) - :doc:`Xiaomi Chuangmi Plug V3 (1 Socket, 2 USB Ports) ` (:class:`miio.chuangmi_plug`) diff --git a/docs/miio.rst b/docs/miio.rst index 70b33edb7..4f5b289ab 100644 --- a/docs/miio.rst +++ b/docs/miio.rst @@ -45,6 +45,15 @@ miio\.airqualitymonitor module :show-inheritance: :undoc-members: +miio\.aqaracamera module +------------------------ + +.. automodule:: miio.aqaracamera + :members: + :show-inheritance: + :undoc-members: + + miio\.ceil module -----------------