From 0d3383026200ba3c4b79137043e4ec4b8a71c305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Sun, 4 Oct 2020 16:23:59 +0200 Subject: [PATCH 01/30] Update: WIP config fow --- custom_components/tapo_control/__init__.py | 16 +--------------- custom_components/tapo_control/config_flow.py | 14 ++++++++++++++ custom_components/tapo_control/const.py | 15 +++++++++++++++ custom_components/tapo_control/manifest.json | 3 ++- 4 files changed, 32 insertions(+), 16 deletions(-) create mode 100644 custom_components/tapo_control/config_flow.py create mode 100644 custom_components/tapo_control/const.py diff --git a/custom_components/tapo_control/__init__.py b/custom_components/tapo_control/__init__.py index f7915e9..ee5220a 100644 --- a/custom_components/tapo_control/__init__.py +++ b/custom_components/tapo_control/__init__.py @@ -6,23 +6,9 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_time_interval from datetime import timedelta +from .const import * _LOGGER = logging.getLogger(__name__) -DOMAIN = "tapo_control" -ALARM_MODE = "alarm_mode" -PRESET = "preset" -LIGHT = "light" -SOUND = "sound" -PRIVACY_MODE = "privacy_mode" -LED_MODE = "led_mode" -NAME = "name" -DISTANCE = "distance" -TILT = "tilt" -PAN = "pan" -ENTITY_ID = "entity_id" -MOTION_DETECTION_MODE = "motion_detection_mode" -AUTO_TRACK_MODE = "auto_track_mode" -DEFAULT_SCAN_INTERVAL = 10 CONFIG_SCHEMA = vol.Schema( { diff --git a/custom_components/tapo_control/config_flow.py b/custom_components/tapo_control/config_flow.py new file mode 100644 index 0000000..f5fed87 --- /dev/null +++ b/custom_components/tapo_control/config_flow.py @@ -0,0 +1,14 @@ +from homeassistant import config_entries +from .const import DOMAIN +import voluptuous as vol + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + VERSION = 1 + + async def async_step_user(self, info): + if info is not None: + pass # TODO: process info + + return self.async_show_form( + step_id="user", data_schema=vol.Schema({vol.Required("password"): str}) + ) \ No newline at end of file diff --git a/custom_components/tapo_control/const.py b/custom_components/tapo_control/const.py new file mode 100644 index 0000000..7e1ced0 --- /dev/null +++ b/custom_components/tapo_control/const.py @@ -0,0 +1,15 @@ +DOMAIN = "tapo_control" +ALARM_MODE = "alarm_mode" +PRESET = "preset" +LIGHT = "light" +SOUND = "sound" +PRIVACY_MODE = "privacy_mode" +LED_MODE = "led_mode" +NAME = "name" +DISTANCE = "distance" +TILT = "tilt" +PAN = "pan" +ENTITY_ID = "entity_id" +MOTION_DETECTION_MODE = "motion_detection_mode" +AUTO_TRACK_MODE = "auto_track_mode" +DEFAULT_SCAN_INTERVAL = 10 \ No newline at end of file diff --git a/custom_components/tapo_control/manifest.json b/custom_components/tapo_control/manifest.json index c75a80c..18fe541 100644 --- a/custom_components/tapo_control/manifest.json +++ b/custom_components/tapo_control/manifest.json @@ -5,6 +5,7 @@ "dependencies": [], "issue_tracker": "https://github.com/JurajNyiri/HomeAssistant-Tapo-Control/issues", "codeowners": ["@JurajNyiri"], - "requirements": ["pytapo==0.6"] + "requirements": ["pytapo==0.6"], + "config_flow": true } \ No newline at end of file From a44d2113f5191303e321f21c12e40c861600152d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Mon, 5 Oct 2020 01:03:37 +0200 Subject: [PATCH 02/30] Update: Move common functions to utils --- custom_components/tapo_control/__init__.py | 44 +-------------------- custom_components/tapo_control/utils.py | 45 ++++++++++++++++++++++ 2 files changed, 47 insertions(+), 42 deletions(-) create mode 100644 custom_components/tapo_control/utils.py diff --git a/custom_components/tapo_control/__init__.py b/custom_components/tapo_control/__init__.py index 90c6107..4a05a12 100644 --- a/custom_components/tapo_control/__init__.py +++ b/custom_components/tapo_control/__init__.py @@ -4,10 +4,10 @@ import re import voluptuous as vol import homeassistant.helpers.config_validation as cv -import unidecode from homeassistant.helpers.event import track_time_interval from datetime import timedelta from .const import * +from .utils import * _LOGGER = logging.getLogger(__name__) @@ -49,42 +49,6 @@ def manualUpdate(entity_id, tapoConnector): hass.states.set(entity_id, tapoData[entity_id]['state'], tapoData[entity_id]['attributes']) - def getIncrement(entity_id): - lastNum = entity_id[entity_id.rindex('_')+1:] - if(lastNum.isnumeric()): - return int(lastNum)+1 - return 1 - - def addTapoEntityID(requested_entity_id, requested_value): - regex = r"^"+requested_entity_id.replace(".","\.")+"_[0-9]+$" - if(requested_entity_id in tapo): - biggestIncrement = 0 - for id in tapo: - r1 = re.findall(regex,id) - if r1: - inc = getIncrement(requested_entity_id) - if(inc > biggestIncrement): - biggestIncrement = inc - if(biggestIncrement == 0): - oldVal = tapo[requested_entity_id] - tapo.pop(requested_entity_id, None) - tapo[requested_entity_id+"_1"] = oldVal - tapo[requested_entity_id+"_2"] = requested_value - else: - tapo[requested_entity_id+"_"+str(biggestIncrement)] = requested_value - else: - biggestIncrement = 0 - for id in tapo: - r1 = re.findall(regex,id) - if r1: - inc = getIncrement(id) - if(inc > biggestIncrement): - biggestIncrement = inc - if(biggestIncrement == 0): - tapo[requested_entity_id] = requested_value - else: - tapo[requested_entity_id+"_"+str(biggestIncrement)] = requested_value - def handle_ptz(call): if ENTITY_ID in call.data: entity_id = call.data.get(ENTITY_ID) @@ -314,10 +278,6 @@ def handle_delete_preset(call): else: _LOGGER.error("Please specify "+ENTITY_ID+" value.") - def generateEntityIDFromName(name): - str = unidecode.unidecode(name.rstrip().replace(".","_").replace(" ", "_").lower()) - str = re.sub("_"+'{2,}',"_",''.join(filter(ENTITY_CHAR_WHITELIST.__contains__, str))) - return DOMAIN+"."+str for camera in config[DOMAIN]: host = camera[CONF_HOST] @@ -329,7 +289,7 @@ def generateEntityIDFromName(name): entity_id = generateEntityIDFromName(basicInfo['device_info']['basic_info']['device_alias']) # handles conflicts if entity_id the same - addTapoEntityID(entity_id,tapoConnector) + addTapoEntityID(tapo, entity_id,tapoConnector) for entity_id in tapo: diff --git a/custom_components/tapo_control/utils.py b/custom_components/tapo_control/utils.py new file mode 100644 index 0000000..300ce67 --- /dev/null +++ b/custom_components/tapo_control/utils.py @@ -0,0 +1,45 @@ +import re +import unidecode +from .const import * + +def getIncrement(entity_id): + lastNum = entity_id[entity_id.rindex('_')+1:] + if(lastNum.isnumeric()): + return int(lastNum)+1 + return 1 + +def addTapoEntityID(tapo, requested_entity_id, requested_value): + regex = r"^"+requested_entity_id.replace(".","\.")+"_[0-9]+$" + if(requested_entity_id in tapo): + biggestIncrement = 0 + for id in tapo: + r1 = re.findall(regex,id) + if r1: + inc = getIncrement(requested_entity_id) + if(inc > biggestIncrement): + biggestIncrement = inc + if(biggestIncrement == 0): + oldVal = tapo[requested_entity_id] + tapo.pop(requested_entity_id, None) + tapo[requested_entity_id+"_1"] = oldVal + tapo[requested_entity_id+"_2"] = requested_value + else: + tapo[requested_entity_id+"_"+str(biggestIncrement)] = requested_value + else: + biggestIncrement = 0 + for id in tapo: + r1 = re.findall(regex,id) + if r1: + inc = getIncrement(id) + if(inc > biggestIncrement): + biggestIncrement = inc + if(biggestIncrement == 0): + tapo[requested_entity_id] = requested_value + else: + tapo[requested_entity_id+"_"+str(biggestIncrement)] = requested_value + return tapo + +def generateEntityIDFromName(name): + str = unidecode.unidecode(name.rstrip().replace(".","_").replace(" ", "_").lower()) + str = re.sub("_"+'{2,}',"_",''.join(filter(ENTITY_CHAR_WHITELIST.__contains__, str))) + return DOMAIN+"."+str \ No newline at end of file From d71e8eaf0d934e947138b0d65d759609b0e63e90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Mon, 5 Oct 2020 01:04:10 +0200 Subject: [PATCH 03/30] Add: Working config flow --- custom_components/tapo_control/config_flow.py | 47 +++++++++++++++++-- custom_components/tapo_control/strings.json | 20 ++++++++ .../tapo_control/translations/en.json | 21 +++++++++ 3 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 custom_components/tapo_control/strings.json create mode 100644 custom_components/tapo_control/translations/en.json diff --git a/custom_components/tapo_control/config_flow.py b/custom_components/tapo_control/config_flow.py index f5fed87..f2d7bc6 100644 --- a/custom_components/tapo_control/config_flow.py +++ b/custom_components/tapo_control/config_flow.py @@ -1,14 +1,51 @@ +from pytapo import Tapo from homeassistant import config_entries -from .const import DOMAIN +from homeassistant.const import ( + CONF_IP_ADDRESS, + CONF_USERNAME, + CONF_PASSWORD +) import voluptuous as vol +import logging + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +DEVICE_SCHEMA = vol.Schema( + { + vol.Required(CONF_IP_ADDRESS): str, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str + } +) class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_user(self, info): - if info is not None: - pass # TODO: process info + async def async_step_user(self, user_input=None): + errors = {} + if user_input is not None: + try: + host = user_input[CONF_IP_ADDRESS] + username = user_input[CONF_USERNAME] + password = user_input[CONF_PASSWORD] + + Tapo(host, username, password) + + return self.async_create_entry( + title=host, + data={CONF_IP_ADDRESS: host, CONF_USERNAME: username, CONF_PASSWORD: password,}, + ) + except Exception as e: + if("Failed to establish a new connection" in str(e)): + errors["base"] = "connection_failed" + elif(str(e) == "Invalid authentication data."): + errors["base"] = "invalid_auth" + else: + errors["base"] = "unknown" + _LOGGER.error(e) return self.async_show_form( - step_id="user", data_schema=vol.Schema({vol.Required("password"): str}) + step_id="user", data_schema=DEVICE_SCHEMA, errors=errors ) \ No newline at end of file diff --git a/custom_components/tapo_control/strings.json b/custom_components/tapo_control/strings.json new file mode 100644 index 0000000..12c6304 --- /dev/null +++ b/custom_components/tapo_control/strings.json @@ -0,0 +1,20 @@ +{ + "title": "Tapo: Cameras Control", + "config": { + "step": { + "user": { + "title": "Connect Tapo camera", + "data": { + "ip_address": "IP Address", + "username": "Username", + "password": "Password" + } + } + }, + "error": { + "invalid_auth": "Invalid authentication data", + "unknown": "Unknown error", + "connection_failed": "Connection failed" + } + } +} diff --git a/custom_components/tapo_control/translations/en.json b/custom_components/tapo_control/translations/en.json new file mode 100644 index 0000000..c11a068 --- /dev/null +++ b/custom_components/tapo_control/translations/en.json @@ -0,0 +1,21 @@ +{ + "title": "Tapo: Cameras Control", + "config": { + "step": { + "user": { + "title": "Connect Tapo camera", + "data": { + "ip_address": "IP Address", + "username": "Username", + "password": "Password" + } + } + }, + "error": { + "invalid_auth": "Invalid authentication data", + "unknown": "Unknown error", + "connection_failed": "Connection failed" + } + } + } + \ No newline at end of file From fa5ee0b91a43dc8ff10bc0bdc3719f98b2da3018 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Mon, 5 Oct 2020 01:05:12 +0200 Subject: [PATCH 04/30] Update: Readme for 2.0 --- README.md | 151 +----------------------------------------------------- 1 file changed, 2 insertions(+), 149 deletions(-) diff --git a/README.md b/README.md index 2b05a1d..6460723 100644 --- a/README.md +++ b/README.md @@ -1,150 +1,3 @@ -# HomeAssistant - Tapo: Cameras Control +# WORK IN PROGRESS -Custom component - Tapo: Cameras Control - to control Tapo camera features - -## Installation - -Copy contents of custom_components/tapo_control/ to custom_components/tapo_control/ in your Home Assistant config folder. - -## Installation using HACS - -HACS is a community store for Home Assistant. You can install [HACS](https://github.com/custom-components/hacs) and then install Tapo: Camera Control from the HACS store. - -## Usage - -Add to configuration.yaml: - -``` -tapo_control: - - host: [IP ADDRESS TO TAPO CAMERA] - username: [USERNAME SET IN ADVANCED OPTIONS IN CAMERA APP] - password: [PASSWORD SET IN ADVANCED OPTIONS IN CAMERA APP] -``` - -You are able to add multiple cameras. - -## Services - -This custom component creates tapo_control.* entities in your Home Assistant. Use these entity_id(s) in following service calls. - -
- tapo_control.ptz - - Pan and tilt camera. - - You are also able to use presets and set distance the ptz should travel. - - - **entity_id** Required: Entity to adjust - - **tilt** Optional: Tilt direction. Allowed values: UP, DOWN - - **pan** Optional: Pan direction. Allowed values: RIGHT, LEFT - - **preset** Optional: PTZ preset ID or a Name. See possible presets in entity attributes. - - **distance** Optional: Distance coefficient. Sets how much PTZ should be executed in one request. Allowed values: floating point numbers, 0 to 1 -
- -
- tapo_control.set_privacy_mode - - Sets privacy mode. - - If privacy mode is turned on, camera does not record anything and does not respond to anything other than turning off privacy mode. - - - **entity_id** Required: Entity to set privacy mode for - - **privacy_mode** Required: Sets privacy mode for camera. Possible values: on, off -
- -
- tapo_control.set_alarm_mode - - Sets alarm mode. - - If camera detects motion, it will sound an alarm, blink the LED or both. - - - **entity_id** Required: Entity to set alarm mode for - - **alarm_mode** Required: Sets alarm mode for camera. Possible values: on, off - - **sound** Optional: Sets whether the alarm should use sound on motion detected. Possible values: on, off - - **light** Optional: Sets whether the alarm should use light on motion detected. Possible values: on, off -
- -
- tapo_control.set_led_mode - - Sets LED mode. - - When on, LED is turned on when camera is on. - - When off, LED is always off. - - - **entity_id** Required: Entity to set LED mode for - - **led_mode** Required: Sets LED mode for camera. Possible values: on, off -
- -
- tapo_control.set_motion_detection_mode - - Sets motion detection mode. - - Ability to set "high", "normal" or "low". - - These turn on motion detection and set sensitivity to corresponding values in the app. - - Also ability to set to "off", this turns off motion detection completely. - - Turning motion detection off does not affect settings for recordings so you do not need to re-set those unless you open the settings through the Tapo app. - - Notice: If you use motion detection triggered recording and you turn off motion recording, it will no longer record! - - - **entity_id** Required: Entity to set motion detection mode for - - **motion_detection_mode** Required: Sets motion detection mode for camera. Possible values: high, normal, low, off -
- -
- tapo_control.set_auto_track_mode - - **Warning: This mode is not available in Tapo app and we do not know why. Use at your own risk and please report any success or failures in [Home Assistant: Community Forum](https://community.home-assistant.io/t/tapo-cameras-control/231795).** - - Sets auto track mode. - - With this mode, camera will be adjusting ptz to track whatever moving object it sees. - - Motion detection setting does not affect this mode. - - - **entity_id** Required: Entity to set auto track mode for - - **auto_track_mode** Required: Sets auto track mode for camera. Possible values: on, off -
- -
- tapo_control.reboot - - Reboots the camera - - - **entity_id** Required: Entity to reboot -
- -
- tapo_control.save_preset - - Saves the current PTZ position to a preset - - - **entity_id** Required: Entity to save the preset for - - **name** Required: Name of the preset. Cannot be empty or a number -
- -
- tapo_control.delete_preset - - Deletes a preset - - - **entity_id** Required: Entity to delete the preset for - - **preset** Required: PTZ preset ID or a Name. See possible presets in entity attributes -
- -## Have a comment or a suggestion? - -Please [open a new issue](https://github.com/JurajNyiri/HomeAssistant-Tapo-Control/issues/new), or discuss on [Home Assistant: Community Forum](https://community.home-assistant.io/t/tapo-cameras-control/231795). - -## Thank you - -- [Dale Pavey](https://research.nccgroup.com/2020/07/31/lights-camera-hacked-an-insight-into-the-world-of-popular-ip-cameras/) from NCC Group for the initial research on the Tapo C200 -- [likaci](https://github.com/likaci) and [his github repository](https://github.com/likaci/mercury-ipc-control) for the research on the Mercury camera on which tapo is based -- [Tim Zhang](https://github.com/ttimasdf) for additional research for Mercury camera on [his github repository](https://github.com/ttimasdf/mercury-ipc-control) -- [Gábor Szabados](https://github.com/GSzabados) for doing research and gathering all the information above in [Home Assistant Community forum](https://community.home-assistant.io/t/use-pan-tilt-function-for-tp-link-tapo-c200-from-home-assistant/170143/18) \ No newline at end of file +Do not use, this branch is unstable and possibly does not work. \ No newline at end of file From 2746fc072da85fb680854f5bcd24863aedfdee46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Mon, 5 Oct 2020 14:21:27 +0200 Subject: [PATCH 05/30] Add: New architecture --- custom_components/tapo_control/__init__.py | 55 ++++++++++------- custom_components/tapo_control/camera.py | 70 ++++++++++++++++++++++ custom_components/tapo_control/const.py | 15 ++++- 3 files changed, 116 insertions(+), 24 deletions(-) create mode 100644 custom_components/tapo_control/camera.py diff --git a/custom_components/tapo_control/__init__.py b/custom_components/tapo_control/__init__.py index 4a05a12..44081a0 100644 --- a/custom_components/tapo_control/__init__.py +++ b/custom_components/tapo_control/__init__.py @@ -1,34 +1,42 @@ from pytapo import Tapo -from homeassistant.const import (CONF_HOST, CONF_USERNAME, CONF_PASSWORD) +from homeassistant.const import (CONF_IP_ADDRESS, CONF_USERNAME, CONF_PASSWORD) +from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.exceptions import ConfigEntryNotReady import logging -import re -import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import track_time_interval -from datetime import timedelta from .const import * from .utils import * _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.All( - cv.ensure_list, - [ - vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string - } - ) - ], +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Tapo: Cameras Control component from YAML.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up the Tapo: Cameras Control component from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + + host = entry.data.get(CONF_IP_ADDRESS) + username = entry.data.get(CONF_USERNAME) + password = entry.data.get(CONF_PASSWORD) + try: + tapoController = Tapo(host, username, password) + + hass.data[DOMAIN][entry.entry_id] = tapoController + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "camera") ) - }, - extra=vol.ALLOW_EXTRA, -) + except Exception as e: + _LOGGER.error("Unable to connect to Tapo: Cameras Control controller: %s", str(e)) + raise ConfigEntryNotReady + + return True + +""" tapo = {} tapoData = {} @@ -308,4 +316,5 @@ def handle_delete_preset(call): track_time_interval(hass, update, timedelta(seconds=DEFAULT_SCAN_INTERVAL)) - return True \ No newline at end of file + return True +""" \ No newline at end of file diff --git a/custom_components/tapo_control/camera.py b/custom_components/tapo_control/camera.py new file mode 100644 index 0000000..def5695 --- /dev/null +++ b/custom_components/tapo_control/camera.py @@ -0,0 +1,70 @@ +from .const import * +from .utils import * +from homeassistant.core import HomeAssistant +from typing import Callable +from homeassistant.helpers.entity import Entity +from pytapo import Tapo +from homeassistant.util import slugify +from homeassistant.helpers import entity_platform +import logging + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass: HomeAssistant, entry: dict, async_add_entities: Callable): + async_add_entities([TapoCameraControl(entry, hass.data[DOMAIN][entry.entry_id])]) + + platform = entity_platform.current_platform.get() + platform.async_register_entity_service( + SERVICE_SET_LED_MODE, SCHEMA_SERVICE_SET_LED_MODE, "set_led_mode", + ) + + +class TapoCameraControl(Entity): + def __init__(self, entry: dict, controller: Tapo): + self._basic_info = controller.getBasicInfo()['device_info']['basic_info'] + self._name = self._basic_info['device_alias'] + self._mac = self._basic_info['mac'] + self._controller = controller + self._attributes = self._basic_info + self._entry = entry + self._state = "Monitoring" + if(not self._basic_info['device_model'] in DEVICES_WITH_NO_PRESETS): + self._attributes['presets'] = controller.getPresets() + + @property + def icon(self) -> str: + return "mdi:cctv" + + @property + def name(self) -> str: + return f"{self._name}" + + @property + def unique_id(self) -> str: + return slugify(f"{self._mac}_tapo_control") + + @property + def device_state_attributes(self): + return self._attributes + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + def update(self): + self.manualUpdate() + + def manualUpdate(self): + self._basic_info = self._controller.getBasicInfo()['device_info']['basic_info'] + self._name = self._basic_info['device_alias'] + self._attributes = self._basic_info + self._state = "Monitoring" + if(not self._basic_info['device_model'] in DEVICES_WITH_NO_PRESETS): + self._attributes['presets'] = self._controller.getPresets() + + def set_led_mode(self, led_mode: str): + if(led_mode == "on"): + self._controller.setLEDEnabled(True) + else: + self._controller.setLEDEnabled(False) diff --git a/custom_components/tapo_control/const.py b/custom_components/tapo_control/const.py index 8e49108..4c7629d 100644 --- a/custom_components/tapo_control/const.py +++ b/custom_components/tapo_control/const.py @@ -1,3 +1,7 @@ +import voluptuous as vol + +from homeassistant.helpers import config_validation as cv + DOMAIN = "tapo_control" ALARM_MODE = "alarm_mode" PRESET = "preset" @@ -16,4 +20,13 @@ ENTITY_CHAR_WHITELIST = set('abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_') DEVICE_MODEL_C100 = "C100" DEVICE_MODEL_C200 = "C200" -DEVICES_WITH_NO_PRESETS = [DEVICE_MODEL_C100] \ No newline at end of file +DEVICES_WITH_NO_PRESETS = [DEVICE_MODEL_C100] + +TOGGLE_STATES = ["on", "off"] + +SERVICE_SET_LED_MODE = "set_led_mode" + +SCHEMA_SERVICE_SET_LED_MODE = { + vol.Required(ENTITY_ID): cv.string, + vol.Required(LED_MODE): vol.In(TOGGLE_STATES), +} \ No newline at end of file From c7c7ee9d249d4e12f72ad61c9f94ff41c4dd26ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Mon, 5 Oct 2020 14:45:24 +0200 Subject: [PATCH 06/30] Add: ptz --- custom_components/tapo_control/camera.py | 55 ++++++++++++++++++++++-- custom_components/tapo_control/const.py | 12 +++++- 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/custom_components/tapo_control/camera.py b/custom_components/tapo_control/camera.py index def5695..2b16cfe 100644 --- a/custom_components/tapo_control/camera.py +++ b/custom_components/tapo_control/camera.py @@ -17,6 +17,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: dict, async_add_entities platform.async_register_entity_service( SERVICE_SET_LED_MODE, SCHEMA_SERVICE_SET_LED_MODE, "set_led_mode", ) + platform.async_register_entity_service( + SERVICE_PTZ, SCHEMA_SERVICE_PTZ, "ptz", + ) class TapoCameraControl(Entity): @@ -28,8 +31,10 @@ def __init__(self, entry: dict, controller: Tapo): self._attributes = self._basic_info self._entry = entry self._state = "Monitoring" - if(not self._basic_info['device_model'] in DEVICES_WITH_NO_PRESETS): - self._attributes['presets'] = controller.getPresets() + if(self._basic_info['device_model'] in DEVICES_WITH_NO_PRESETS): + self._attributes['presets'] = {} + else: + self._attributes['presets'] = self._controller.getPresets() @property def icon(self) -> str: @@ -60,7 +65,9 @@ def manualUpdate(self): self._name = self._basic_info['device_alias'] self._attributes = self._basic_info self._state = "Monitoring" - if(not self._basic_info['device_model'] in DEVICES_WITH_NO_PRESETS): + if(self._basic_info['device_model'] in DEVICES_WITH_NO_PRESETS): + self._attributes['presets'] = {} + else: self._attributes['presets'] = self._controller.getPresets() def set_led_mode(self, led_mode: str): @@ -68,3 +75,45 @@ def set_led_mode(self, led_mode: str): self._controller.setLEDEnabled(True) else: self._controller.setLEDEnabled(False) + + def ptz(self, tilt = None, pan = None, preset = None, distance = None): + if preset: + if(preset.isnumeric()): + self._controller.setPreset(preset) + else: + foundKey = False + for key, value in self._attributes['presets'].items(): + if value == preset: + foundKey = key + if(foundKey): + self._controller.setPreset(foundKey) + else: + _LOGGER.error("Preset "+preset+" does not exist.") + elif tilt: + if distance: + distance = float(distance) + if(distance >= 0 and distance <= 1): + degrees = 68 * distance + else: + degrees = 5 + else: + degrees = 5 + if tilt == "UP": + self._controller.moveMotor(0,degrees) + else: + self._controller.moveMotor(0,-degrees) + elif pan: + if distance: + distance = float(distance) + if(distance >= 0 and distance <= 1): + degrees = 360 * distance + else: + degrees = 5 + else: + degrees = 5 + if pan == "RIGHT": + self._controller.moveMotor(degrees,0) + else: + self._controller.moveMotor(-degrees,0) + else: + _LOGGER.error("Incorrect additional PTZ properties. You need to specify at least one of " + TILT + ", " + PAN + ", " + PRESET + ".") diff --git a/custom_components/tapo_control/const.py b/custom_components/tapo_control/const.py index 4c7629d..ed92c7e 100644 --- a/custom_components/tapo_control/const.py +++ b/custom_components/tapo_control/const.py @@ -24,9 +24,17 @@ TOGGLE_STATES = ["on", "off"] -SERVICE_SET_LED_MODE = "set_led_mode" +SERVICE_PTZ = "ptz" +SCHEMA_SERVICE_PTZ = { + vol.Required(ENTITY_ID): cv.string, + vol.Optional(TILT): vol.In(["UP", "DOWN"]), + vol.Optional(PAN): vol.In(["RIGHT", "LEFT"]), + vol.Optional(PRESET): cv.string, + vol.Optional(DISTANCE): cv.string +} +SERVICE_SET_LED_MODE = "set_led_mode" SCHEMA_SERVICE_SET_LED_MODE = { vol.Required(ENTITY_ID): cv.string, - vol.Required(LED_MODE): vol.In(TOGGLE_STATES), + vol.Required(LED_MODE): vol.In(TOGGLE_STATES) } \ No newline at end of file From dee1c7647a790ecb4d937d25fbaf4d3029eb33bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Mon, 5 Oct 2020 14:50:34 +0200 Subject: [PATCH 07/30] Add: set_privacy_mode --- custom_components/tapo_control/camera.py | 21 +++++++++++++++------ custom_components/tapo_control/const.py | 6 ++++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/custom_components/tapo_control/camera.py b/custom_components/tapo_control/camera.py index 2b16cfe..b8613d8 100644 --- a/custom_components/tapo_control/camera.py +++ b/custom_components/tapo_control/camera.py @@ -17,6 +17,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: dict, async_add_entities platform.async_register_entity_service( SERVICE_SET_LED_MODE, SCHEMA_SERVICE_SET_LED_MODE, "set_led_mode", ) + platform.async_register_entity_service( + SERVICE_SET_PRIVACY_MODE, SCHEMA_SERVICE_SET_PRIVACY_MODE, "set_privacy_mode", + ) platform.async_register_entity_service( SERVICE_PTZ, SCHEMA_SERVICE_PTZ, "ptz", ) @@ -70,12 +73,6 @@ def manualUpdate(self): else: self._attributes['presets'] = self._controller.getPresets() - def set_led_mode(self, led_mode: str): - if(led_mode == "on"): - self._controller.setLEDEnabled(True) - else: - self._controller.setLEDEnabled(False) - def ptz(self, tilt = None, pan = None, preset = None, distance = None): if preset: if(preset.isnumeric()): @@ -117,3 +114,15 @@ def ptz(self, tilt = None, pan = None, preset = None, distance = None): self._controller.moveMotor(-degrees,0) else: _LOGGER.error("Incorrect additional PTZ properties. You need to specify at least one of " + TILT + ", " + PAN + ", " + PRESET + ".") + + def set_led_mode(self, led_mode: str): + if(led_mode == "on"): + self._controller.setLEDEnabled(True) + else: + self._controller.setLEDEnabled(False) + + def set_privacy_mode(self, privacy_mode: str): + if(privacy_mode == "on"): + self._controller.setPrivacyMode(True) + else: + self._controller.setPrivacyMode(False) diff --git a/custom_components/tapo_control/const.py b/custom_components/tapo_control/const.py index ed92c7e..1f41537 100644 --- a/custom_components/tapo_control/const.py +++ b/custom_components/tapo_control/const.py @@ -33,6 +33,12 @@ vol.Optional(DISTANCE): cv.string } +SERVICE_SET_PRIVACY_MODE = "set_privacy_mode" +SCHEMA_SERVICE_SET_PRIVACY_MODE = { + vol.Required(ENTITY_ID): cv.string, + vol.Required(PRIVACY_MODE): vol.In(TOGGLE_STATES) +} + SERVICE_SET_LED_MODE = "set_led_mode" SCHEMA_SERVICE_SET_LED_MODE = { vol.Required(ENTITY_ID): cv.string, From 927363694e738fe93330c5c816309fdd6d07e19f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Mon, 5 Oct 2020 14:58:17 +0200 Subject: [PATCH 08/30] Add: set_alarm_mode --- custom_components/tapo_control/camera.py | 25 ++++++++++++++++++------ custom_components/tapo_control/const.py | 9 +++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/custom_components/tapo_control/camera.py b/custom_components/tapo_control/camera.py index b8613d8..3eba4f9 100644 --- a/custom_components/tapo_control/camera.py +++ b/custom_components/tapo_control/camera.py @@ -23,6 +23,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: dict, async_add_entities platform.async_register_entity_service( SERVICE_PTZ, SCHEMA_SERVICE_PTZ, "ptz", ) + platform.async_register_entity_service( + SERVICE_SET_ALARM_MODE, SCHEMA_SERVICE_SET_ALARM_MODE, "set_alarm_mode", + ) class TapoCameraControl(Entity): @@ -115,14 +118,24 @@ def ptz(self, tilt = None, pan = None, preset = None, distance = None): else: _LOGGER.error("Incorrect additional PTZ properties. You need to specify at least one of " + TILT + ", " + PAN + ", " + PRESET + ".") - def set_led_mode(self, led_mode: str): - if(led_mode == "on"): - self._controller.setLEDEnabled(True) - else: - self._controller.setLEDEnabled(False) - def set_privacy_mode(self, privacy_mode: str): if(privacy_mode == "on"): self._controller.setPrivacyMode(True) else: self._controller.setPrivacyMode(False) + + def set_alarm_mode(self, alarm_mode, sound = None, light = None): + if(not light): + light = "on" + if(not sound): + sound = "on" + if(alarm_mode == "on"): + self._controller.setAlarm(True, True if sound == "on" else False, True if light == "on" else False) + else: + self._controller.setAlarm(False, True if sound == "on" else False, True if light == "on" else False) + + def set_led_mode(self, led_mode: str): + if(led_mode == "on"): + self._controller.setLEDEnabled(True) + else: + self._controller.setLEDEnabled(False) diff --git a/custom_components/tapo_control/const.py b/custom_components/tapo_control/const.py index 1f41537..d3d4222 100644 --- a/custom_components/tapo_control/const.py +++ b/custom_components/tapo_control/const.py @@ -39,6 +39,15 @@ vol.Required(PRIVACY_MODE): vol.In(TOGGLE_STATES) } +SERVICE_SET_ALARM_MODE = "set_alarm_mode" +SCHEMA_SERVICE_SET_ALARM_MODE = { + vol.Required(ENTITY_ID): cv.string, + vol.Required(ALARM_MODE): vol.In(TOGGLE_STATES), + vol.Optional(SOUND): vol.In(TOGGLE_STATES), + vol.Optional(LIGHT): vol.In(TOGGLE_STATES) +} + + SERVICE_SET_LED_MODE = "set_led_mode" SCHEMA_SERVICE_SET_LED_MODE = { vol.Required(ENTITY_ID): cv.string, From 9e2ca8cc7aa8872a6fffe4d8dd8cb20aedf25657 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Mon, 5 Oct 2020 15:06:29 +0200 Subject: [PATCH 09/30] Add: set_motion_detection_mode --- custom_components/tapo_control/camera.py | 9 +++++++++ custom_components/tapo_control/const.py | 7 ++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/custom_components/tapo_control/camera.py b/custom_components/tapo_control/camera.py index 3eba4f9..ffeec73 100644 --- a/custom_components/tapo_control/camera.py +++ b/custom_components/tapo_control/camera.py @@ -26,6 +26,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: dict, async_add_entities platform.async_register_entity_service( SERVICE_SET_ALARM_MODE, SCHEMA_SERVICE_SET_ALARM_MODE, "set_alarm_mode", ) + platform.async_register_entity_service( + SERVICE_SET_MOTION_DETECTION_MODE, SCHEMA_SERVICE_SET_MOTION_DETECTION_MODE, "set_motion_detection_mode", + ) class TapoCameraControl(Entity): @@ -139,3 +142,9 @@ def set_led_mode(self, led_mode: str): self._controller.setLEDEnabled(True) else: self._controller.setLEDEnabled(False) + + def set_motion_detection_mode(self, motion_detection_mode): + if(motion_detection_mode == "off"): + self._controller.setMotionDetection(False) + else: + self._controller.setMotionDetection(True, motion_detection_mode) diff --git a/custom_components/tapo_control/const.py b/custom_components/tapo_control/const.py index d3d4222..06cd4eb 100644 --- a/custom_components/tapo_control/const.py +++ b/custom_components/tapo_control/const.py @@ -47,9 +47,14 @@ vol.Optional(LIGHT): vol.In(TOGGLE_STATES) } - SERVICE_SET_LED_MODE = "set_led_mode" SCHEMA_SERVICE_SET_LED_MODE = { vol.Required(ENTITY_ID): cv.string, vol.Required(LED_MODE): vol.In(TOGGLE_STATES) +} + +SERVICE_SET_MOTION_DETECTION_MODE = "set_motion_detection_mode" +SCHEMA_SERVICE_SET_MOTION_DETECTION_MODE = { + vol.Required(ENTITY_ID): cv.string, + vol.Required(MOTION_DETECTION_MODE): vol.In(["high","normal","low","off"]) } \ No newline at end of file From 8e6db3cbb1b9e567561e4d3a65e741ca2b66aa3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Mon, 5 Oct 2020 15:13:31 +0200 Subject: [PATCH 10/30] Add: set_auto_track_mode --- custom_components/tapo_control/camera.py | 9 +++++++++ custom_components/tapo_control/const.py | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/custom_components/tapo_control/camera.py b/custom_components/tapo_control/camera.py index ffeec73..7b4b1d8 100644 --- a/custom_components/tapo_control/camera.py +++ b/custom_components/tapo_control/camera.py @@ -29,6 +29,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: dict, async_add_entities platform.async_register_entity_service( SERVICE_SET_MOTION_DETECTION_MODE, SCHEMA_SERVICE_SET_MOTION_DETECTION_MODE, "set_motion_detection_mode", ) + platform.async_register_entity_service( + SERVICE_SET_AUTO_TRACK_MODE, SCHEMA_SERVICE_SET_AUTO_TRACK_MODE, "set_auto_track_mode", + ) class TapoCameraControl(Entity): @@ -148,3 +151,9 @@ def set_motion_detection_mode(self, motion_detection_mode): self._controller.setMotionDetection(False) else: self._controller.setMotionDetection(True, motion_detection_mode) + + def set_auto_track_mode(self, auto_track_mode: str): + if(auto_track_mode == "on"): + self._controller.setAutoTrackTarget(True) + else: + self._controller.setAutoTrackTarget(False) diff --git a/custom_components/tapo_control/const.py b/custom_components/tapo_control/const.py index 06cd4eb..9cd90bc 100644 --- a/custom_components/tapo_control/const.py +++ b/custom_components/tapo_control/const.py @@ -57,4 +57,10 @@ SCHEMA_SERVICE_SET_MOTION_DETECTION_MODE = { vol.Required(ENTITY_ID): cv.string, vol.Required(MOTION_DETECTION_MODE): vol.In(["high","normal","low","off"]) +} + +SERVICE_SET_AUTO_TRACK_MODE = "set_auto_track_mode" +SCHEMA_SERVICE_SET_AUTO_TRACK_MODE = { + vol.Required(ENTITY_ID): cv.string, + vol.Required(AUTO_TRACK_MODE): vol.In(TOGGLE_STATES) } \ No newline at end of file From 9a1011a40276dae17f53152f11a773c147f46d76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Mon, 5 Oct 2020 15:18:07 +0200 Subject: [PATCH 11/30] Add: reboot --- custom_components/tapo_control/camera.py | 6 ++++++ custom_components/tapo_control/const.py | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/custom_components/tapo_control/camera.py b/custom_components/tapo_control/camera.py index 7b4b1d8..5690e75 100644 --- a/custom_components/tapo_control/camera.py +++ b/custom_components/tapo_control/camera.py @@ -32,6 +32,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: dict, async_add_entities platform.async_register_entity_service( SERVICE_SET_AUTO_TRACK_MODE, SCHEMA_SERVICE_SET_AUTO_TRACK_MODE, "set_auto_track_mode", ) + platform.async_register_entity_service( + SERVICE_REBOOT, SCHEMA_SERVICE_REBOOT, "reboot", + ) class TapoCameraControl(Entity): @@ -157,3 +160,6 @@ def set_auto_track_mode(self, auto_track_mode: str): self._controller.setAutoTrackTarget(True) else: self._controller.setAutoTrackTarget(False) + + def reboot(self): + self._controller.reboot() diff --git a/custom_components/tapo_control/const.py b/custom_components/tapo_control/const.py index 9cd90bc..2e0472d 100644 --- a/custom_components/tapo_control/const.py +++ b/custom_components/tapo_control/const.py @@ -63,4 +63,9 @@ SCHEMA_SERVICE_SET_AUTO_TRACK_MODE = { vol.Required(ENTITY_ID): cv.string, vol.Required(AUTO_TRACK_MODE): vol.In(TOGGLE_STATES) +} + +SERVICE_REBOOT = "reboot" +SCHEMA_SERVICE_REBOOT = { + vol.Required(ENTITY_ID): cv.string } \ No newline at end of file From 0db6249358274ecba25e0a9bcf853ddd208ad74d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Mon, 5 Oct 2020 15:23:46 +0200 Subject: [PATCH 12/30] Add: save_preset --- custom_components/tapo_control/camera.py | 10 ++++++++++ custom_components/tapo_control/const.py | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/custom_components/tapo_control/camera.py b/custom_components/tapo_control/camera.py index 5690e75..1008b97 100644 --- a/custom_components/tapo_control/camera.py +++ b/custom_components/tapo_control/camera.py @@ -35,6 +35,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: dict, async_add_entities platform.async_register_entity_service( SERVICE_REBOOT, SCHEMA_SERVICE_REBOOT, "reboot", ) + platform.async_register_entity_service( + SERVICE_SAVE_PRESET, SCHEMA_SERVICE_SAVE_PRESET, "save_preset", + ) class TapoCameraControl(Entity): @@ -163,3 +166,10 @@ def set_auto_track_mode(self, auto_track_mode: str): def reboot(self): self._controller.reboot() + + def save_preset(self, name): + if(not name == "" and not name.isnumeric()): + self._controller.savePreset(name) + self.manualUpdate() + else: + _LOGGER.error("Incorrect "+NAME+" value. It cannot be empty or a number.") diff --git a/custom_components/tapo_control/const.py b/custom_components/tapo_control/const.py index 2e0472d..6bccb3a 100644 --- a/custom_components/tapo_control/const.py +++ b/custom_components/tapo_control/const.py @@ -68,4 +68,10 @@ SERVICE_REBOOT = "reboot" SCHEMA_SERVICE_REBOOT = { vol.Required(ENTITY_ID): cv.string +} + +SERVICE_SAVE_PRESET = "save_preset" +SCHEMA_SERVICE_SAVE_PRESET = { + vol.Required(ENTITY_ID): cv.string, + vol.Required(NAME): cv.string } \ No newline at end of file From 91846064eee8a5b480840ed332377fcacc0cdd5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Mon, 5 Oct 2020 15:32:11 +0200 Subject: [PATCH 13/30] Add: delete_preset --- custom_components/tapo_control/camera.py | 19 +++++++++++++++++++ custom_components/tapo_control/const.py | 6 ++++++ 2 files changed, 25 insertions(+) diff --git a/custom_components/tapo_control/camera.py b/custom_components/tapo_control/camera.py index 1008b97..f263922 100644 --- a/custom_components/tapo_control/camera.py +++ b/custom_components/tapo_control/camera.py @@ -38,6 +38,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: dict, async_add_entities platform.async_register_entity_service( SERVICE_SAVE_PRESET, SCHEMA_SERVICE_SAVE_PRESET, "save_preset", ) + platform.async_register_entity_service( + SERVICE_DELETE_PRESET, SCHEMA_SERVICE_DELETE_PRESET, "delete_preset", + ) class TapoCameraControl(Entity): @@ -173,3 +176,19 @@ def save_preset(self, name): self.manualUpdate() else: _LOGGER.error("Incorrect "+NAME+" value. It cannot be empty or a number.") + + def delete_preset(self, preset): + if(preset.isnumeric()): + self._controller.deletePreset(preset) + self.manualUpdate() + else: + foundKey = False + for key, value in self._attributes['presets'].items(): + if value == preset: + foundKey = key + if(foundKey): + self._controller.deletePreset(foundKey) + self.manualUpdate() + else: + _LOGGER.error("Preset "+preset+" does not exist.") + \ No newline at end of file diff --git a/custom_components/tapo_control/const.py b/custom_components/tapo_control/const.py index 6bccb3a..97a0f53 100644 --- a/custom_components/tapo_control/const.py +++ b/custom_components/tapo_control/const.py @@ -74,4 +74,10 @@ SCHEMA_SERVICE_SAVE_PRESET = { vol.Required(ENTITY_ID): cv.string, vol.Required(NAME): cv.string +} + +SERVICE_DELETE_PRESET = "delete_preset" +SCHEMA_SERVICE_DELETE_PRESET = { + vol.Required(ENTITY_ID): cv.string, + vol.Required(PRESET): cv.string } \ No newline at end of file From 4a88963114f6ba3bcab9bdd7368cb4f09dcdc162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Mon, 5 Oct 2020 15:32:53 +0200 Subject: [PATCH 14/30] Update: Change state to idle --- custom_components/tapo_control/camera.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/tapo_control/camera.py b/custom_components/tapo_control/camera.py index f263922..78d36ed 100644 --- a/custom_components/tapo_control/camera.py +++ b/custom_components/tapo_control/camera.py @@ -51,7 +51,7 @@ def __init__(self, entry: dict, controller: Tapo): self._controller = controller self._attributes = self._basic_info self._entry = entry - self._state = "Monitoring" + self._state = "idle" if(self._basic_info['device_model'] in DEVICES_WITH_NO_PRESETS): self._attributes['presets'] = {} else: @@ -85,7 +85,7 @@ def manualUpdate(self): self._basic_info = self._controller.getBasicInfo()['device_info']['basic_info'] self._name = self._basic_info['device_alias'] self._attributes = self._basic_info - self._state = "Monitoring" + self._state = "idle" if(self._basic_info['device_model'] in DEVICES_WITH_NO_PRESETS): self._attributes['presets'] = {} else: From c1600962d50841f951ed81d3e4a34c8226e9c52c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Mon, 5 Oct 2020 15:34:02 +0200 Subject: [PATCH 15/30] Remove: Old code --- custom_components/tapo_control/__init__.py | 285 +-------------------- 1 file changed, 1 insertion(+), 284 deletions(-) diff --git a/custom_components/tapo_control/__init__.py b/custom_components/tapo_control/__init__.py index 44081a0..e3a09f2 100644 --- a/custom_components/tapo_control/__init__.py +++ b/custom_components/tapo_control/__init__.py @@ -34,287 +34,4 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): _LOGGER.error("Unable to connect to Tapo: Cameras Control controller: %s", str(e)) raise ConfigEntryNotReady - return True - -""" -tapo = {} -tapoData = {} - -def setup(hass, config): - def update(event_time): - for entity_id in tapo: - tapoConnector = tapo[entity_id] - manualUpdate(entity_id, tapoConnector) - - def manualUpdate(entity_id, tapoConnector): - basicInfo = tapoConnector.getBasicInfo() - attributes = basicInfo['device_info']['basic_info'] - if(not basicInfo['device_info']['basic_info']['device_model'] in DEVICES_WITH_NO_PRESETS): - attributes['presets'] = tapoConnector.getPresets() - tapoData[entity_id] = {} - tapoData[entity_id]['state'] = "monitoring" # todo: better state - tapoData[entity_id]['attributes'] = attributes - - hass.states.set(entity_id, tapoData[entity_id]['state'], tapoData[entity_id]['attributes']) - - def handle_ptz(call): - if ENTITY_ID in call.data: - entity_id = call.data.get(ENTITY_ID) - if(isinstance(entity_id, list)): - entity_id = entity_id[0] - if entity_id in tapo: - if PRESET in call.data: - preset = str(call.data.get(PRESET)) - if(preset.isnumeric()): - tapo[entity_id].setPreset(preset) - else: - foundKey = False - presets = tapoData[entity_id]['attributes']['presets'] - for key, value in presets.items(): - if value == preset: - foundKey = key - if(foundKey): - tapo[entity_id].setPreset(foundKey) - else: - _LOGGER.error("Preset "+preset+" does not exist.") - elif TILT in call.data: - tilt = call.data.get(TILT) - if DISTANCE in call.data: - distance = float(call.data.get(DISTANCE)) - if(distance >= 0 and distance <= 1): - degrees = 68 * distance - else: - degrees = 5 - else: - degrees = 5 - if tilt == "UP": - tapo[entity_id].moveMotor(0,degrees) - elif tilt == "DOWN": - tapo[entity_id].moveMotor(0,-degrees) - else: - _LOGGER.error("Incorrect "+TILT+" value. Possible values: UP, DOWN.") - elif PAN in call.data: - pan = call.data.get(PAN) - if DISTANCE in call.data: - distance = float(call.data.get(DISTANCE)) - if(distance >= 0 and distance <= 1): - degrees = 360 * distance - else: - degrees = 5 - else: - degrees = 5 - if pan == "RIGHT": - tapo[entity_id].moveMotor(degrees,0) - elif pan == "LEFT": - tapo[entity_id].moveMotor(-degrees,0) - else: - _LOGGER.error("Incorrect "+PAN+" value. Possible values: RIGHT, LEFT.") - else: - _LOGGER.error("Incorrect additional PTZ properties. You need to specify at least one of " + TILT + ", " + PAN + ", " + PRESET + ".") - else: - _LOGGER.error("Entity "+entity_id+" does not exist.") - else: - _LOGGER.error("Please specify "+ENTITY_ID+" value.") - - def handle_set_privacy_mode(call): - if ENTITY_ID in call.data: - entity_id = call.data.get(ENTITY_ID) - if(isinstance(entity_id, list)): - entity_id = entity_id[0] - if entity_id in tapo: - if(PRIVACY_MODE in call.data): - privacy_mode = call.data.get(PRIVACY_MODE) - if(privacy_mode == "on"): - tapo[entity_id].setPrivacyMode(True) - elif(privacy_mode == "off"): - tapo[entity_id].setPrivacyMode(False) - else: - _LOGGER.error("Incorrect "+PRIVACY_MODE+" value. Possible values: on, off.") - else: - _LOGGER.error("Please specify "+PRIVACY_MODE+" value.") - else: - _LOGGER.error("Entity "+entity_id+" does not exist.") - else: - _LOGGER.error("Please specify "+ENTITY_ID+" value.") - - def handle_set_alarm_mode(call): - if ENTITY_ID in call.data: - entity_id = call.data.get(ENTITY_ID) - if(isinstance(entity_id, list)): - entity_id = entity_id[0] - if entity_id in tapo: - if(ALARM_MODE in call.data): - alarm_mode = call.data.get(ALARM_MODE) - sound = "on" - light = "on" - if(LIGHT in call.data): - light = call.data.get(LIGHT) - if(SOUND in call.data): - sound = call.data.get(SOUND) - if(alarm_mode == "on"): - tapo[entity_id].setAlarm(True, True if sound == "on" else False, True if light == "on" else False) - elif(alarm_mode == "off"): - tapo[entity_id].setAlarm(False, True if sound == "on" else False, True if light == "on" else False) - else: - _LOGGER.error("Incorrect "+ALARM_MODE+" value. Possible values: on, off.") - else: - _LOGGER.error("Please specify "+ALARM_MODE+" value.") - else: - _LOGGER.error("Entity "+entity_id+" does not exist.") - else: - _LOGGER.error("Please specify "+ENTITY_ID+" value.") - - def handle_set_led_mode(call): - if ENTITY_ID in call.data: - entity_id = call.data.get(ENTITY_ID) - if(isinstance(entity_id, list)): - entity_id = entity_id[0] - if entity_id in tapo: - if(LED_MODE in call.data): - led_mode = call.data.get(LED_MODE) - if(led_mode == "on"): - tapo[entity_id].setLEDEnabled(True) - elif(led_mode == "off"): - tapo[entity_id].setLEDEnabled(False) - else: - _LOGGER.error("Incorrect "+LED_MODE+" value. Possible values: on, off.") - else: - _LOGGER.error("Please specify "+LED_MODE+" value.") - else: - _LOGGER.error("Entity "+entity_id+" does not exist.") - else: - _LOGGER.error("Please specify "+ENTITY_ID+" value.") - - def handle_set_motion_detection_mode(call): - if ENTITY_ID in call.data: - entity_id = call.data.get(ENTITY_ID) - if(isinstance(entity_id, list)): - entity_id = entity_id[0] - if entity_id in tapo: - if(MOTION_DETECTION_MODE in call.data): - motion_detection_mode = call.data.get(MOTION_DETECTION_MODE) - if(motion_detection_mode == "high" or motion_detection_mode == "normal" or motion_detection_mode == "low"): - tapo[entity_id].setMotionDetection(True, motion_detection_mode) - elif(motion_detection_mode == "off"): - tapo[entity_id].setMotionDetection(False) - else: - _LOGGER.error("Incorrect "+MOTION_DETECTION_MODE+" value. Possible values: high, normal, low, off.") - else: - _LOGGER.error("Please specify "+MOTION_DETECTION_MODE+" value.") - else: - _LOGGER.error("Entity "+entity_id+" does not exist.") - else: - _LOGGER.error("Please specify "+ENTITY_ID+" value.") - - def handle_set_auto_track_mode(call): - if ENTITY_ID in call.data: - entity_id = call.data.get(ENTITY_ID) - if(isinstance(entity_id, list)): - entity_id = entity_id[0] - if entity_id in tapo: - if(AUTO_TRACK_MODE in call.data): - auto_track_mode = call.data.get(AUTO_TRACK_MODE) - if(auto_track_mode == "on"): - tapo[entity_id].setAutoTrackTarget(True) - elif(auto_track_mode == "off"): - tapo[entity_id].setAutoTrackTarget(False) - else: - _LOGGER.error("Incorrect "+AUTO_TRACK_MODE+" value. Possible values: on, off.") - else: - _LOGGER.error("Please specify "+AUTO_TRACK_MODE+" value.") - else: - _LOGGER.error("Entity "+entity_id+" does not exist.") - else: - _LOGGER.error("Please specify "+ENTITY_ID+" value.") - - def handle_reboot(call): - if ENTITY_ID in call.data: - entity_id = call.data.get(ENTITY_ID) - if(isinstance(entity_id, list)): - entity_id = entity_id[0] - if entity_id in tapo: - tapo[entity_id].reboot() - else: - _LOGGER.error("Entity "+entity_id+" does not exist.") - else: - _LOGGER.error("Please specify "+ENTITY_ID+" value.") - - def handle_save_preset(call): - if ENTITY_ID in call.data: - entity_id = call.data.get(ENTITY_ID) - if(isinstance(entity_id, list)): - entity_id = entity_id[0] - if entity_id in tapo: - if(NAME in call.data): - name = call.data.get(NAME) - if(not name == "" and not name.isnumeric()): - tapo[entity_id].savePreset(name) - update(None) - else: - _LOGGER.error("Incorrect "+NAME+" value. It cannot be empty or a number.") - else: - _LOGGER.error("Please specify "+NAME+" value.") - else: - _LOGGER.error("Entity "+entity_id+" does not exist.") - else: - _LOGGER.error("Please specify "+ENTITY_ID+" value.") - - def handle_delete_preset(call): - if ENTITY_ID in call.data: - entity_id = call.data.get(ENTITY_ID) - if(isinstance(entity_id, list)): - entity_id = entity_id[0] - if entity_id in tapo: - if(PRESET in call.data): - preset = str(call.data.get(PRESET)) - if(preset.isnumeric()): - tapo[entity_id].deletePreset(preset) - else: - foundKey = False - presets = tapoData[entity_id]['attributes']['presets'] - for key, value in presets.items(): - if value == preset: - foundKey = key - if(foundKey): - tapo[entity_id].deletePreset(foundKey) - else: - _LOGGER.error("Preset "+preset+" does not exist.") - else: - _LOGGER.error("Please specify "+PRESET+" value.") - else: - _LOGGER.error("Entity "+entity_id+" does not exist.") - else: - _LOGGER.error("Please specify "+ENTITY_ID+" value.") - - - for camera in config[DOMAIN]: - host = camera[CONF_HOST] - username = camera[CONF_USERNAME] - password = camera[CONF_PASSWORD] - - tapoConnector = Tapo(host, username, password) - basicInfo = tapoConnector.getBasicInfo() - - entity_id = generateEntityIDFromName(basicInfo['device_info']['basic_info']['device_alias']) - # handles conflicts if entity_id the same - addTapoEntityID(tapo, entity_id,tapoConnector) - - - for entity_id in tapo: - tapoConnector = tapo[entity_id] - manualUpdate(entity_id, tapoConnector) - - hass.services.register(DOMAIN, "ptz", handle_ptz) - hass.services.register(DOMAIN, "set_privacy_mode", handle_set_privacy_mode) - hass.services.register(DOMAIN, "set_alarm_mode", handle_set_alarm_mode) - hass.services.register(DOMAIN, "set_led_mode", handle_set_led_mode) - hass.services.register(DOMAIN, "set_motion_detection_mode", handle_set_motion_detection_mode) - hass.services.register(DOMAIN, "set_auto_track_mode", handle_set_auto_track_mode) - hass.services.register(DOMAIN, "reboot", handle_reboot) - hass.services.register(DOMAIN, "save_preset", handle_save_preset) - hass.services.register(DOMAIN, "delete_preset", handle_delete_preset) - - track_time_interval(hass, update, timedelta(seconds=DEFAULT_SCAN_INTERVAL)) - - return True -""" \ No newline at end of file + return True \ No newline at end of file From d003086b7869e7e4c8afd932befe734fcf7cbd9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Mon, 5 Oct 2020 15:36:42 +0200 Subject: [PATCH 16/30] Remove: Unneeded dependency --- custom_components/tapo_control/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/tapo_control/manifest.json b/custom_components/tapo_control/manifest.json index 024507e..7a0a85c 100644 --- a/custom_components/tapo_control/manifest.json +++ b/custom_components/tapo_control/manifest.json @@ -5,7 +5,7 @@ "dependencies": [], "issue_tracker": "https://github.com/JurajNyiri/HomeAssistant-Tapo-Control/issues", "codeowners": ["@JurajNyiri"], - "requirements": ["pytapo==0.10","unidecode"], + "requirements": ["pytapo==0.10"], "config_flow": true } \ No newline at end of file From c7007297f94a2cf06b29d6845c5ad7ffab671bf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Mon, 5 Oct 2020 15:53:17 +0200 Subject: [PATCH 17/30] Update: Rename class --- custom_components/tapo_control/camera.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/tapo_control/camera.py b/custom_components/tapo_control/camera.py index 78d36ed..0b35e4b 100644 --- a/custom_components/tapo_control/camera.py +++ b/custom_components/tapo_control/camera.py @@ -11,7 +11,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: dict, async_add_entities: Callable): - async_add_entities([TapoCameraControl(entry, hass.data[DOMAIN][entry.entry_id])]) + async_add_entities([TapoCam(entry, hass.data[DOMAIN][entry.entry_id])]) platform = entity_platform.current_platform.get() platform.async_register_entity_service( @@ -43,7 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: dict, async_add_entities ) -class TapoCameraControl(Entity): +class TapoCam(Entity): def __init__(self, entry: dict, controller: Tapo): self._basic_info = controller.getBasicInfo()['device_info']['basic_info'] self._name = self._basic_info['device_alias'] From 760f0ee79825969c9a8eafe185fdc7a42840a250 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Mon, 5 Oct 2020 16:31:37 +0200 Subject: [PATCH 18/30] Update: Change name to Tapo: Cameras --- custom_components/tapo_control/__init__.py | 6 +++--- custom_components/tapo_control/camera.py | 3 +-- custom_components/tapo_control/const.py | 2 +- custom_components/tapo_control/manifest.json | 4 ++-- custom_components/tapo_control/services.yaml | 18 +++++++++--------- custom_components/tapo_control/strings.json | 2 +- .../tapo_control/translations/en.json | 2 +- hacs.json | 2 +- info.md | 9 +-------- 9 files changed, 20 insertions(+), 28 deletions(-) diff --git a/custom_components/tapo_control/__init__.py b/custom_components/tapo_control/__init__.py index e3a09f2..977e6b6 100644 --- a/custom_components/tapo_control/__init__.py +++ b/custom_components/tapo_control/__init__.py @@ -10,12 +10,12 @@ _LOGGER = logging.getLogger(__name__) async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Tapo: Cameras Control component from YAML.""" + """Set up the Tapo: Cameras component from YAML.""" return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): - """Set up the Tapo: Cameras Control component from a config entry.""" + """Set up the Tapo: Cameras component from a config entry.""" hass.data.setdefault(DOMAIN, {}) host = entry.data.get(CONF_IP_ADDRESS) @@ -31,7 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) except Exception as e: - _LOGGER.error("Unable to connect to Tapo: Cameras Control controller: %s", str(e)) + _LOGGER.error("Unable to connect to Tapo: Cameras controller: %s", str(e)) raise ConfigEntryNotReady return True \ No newline at end of file diff --git a/custom_components/tapo_control/camera.py b/custom_components/tapo_control/camera.py index 0b35e4b..8769791 100644 --- a/custom_components/tapo_control/camera.py +++ b/custom_components/tapo_control/camera.py @@ -67,7 +67,7 @@ def name(self) -> str: @property def unique_id(self) -> str: - return slugify(f"{self._mac}_tapo_control") + return slugify(f"{self._mac}_tapo") @property def device_state_attributes(self): @@ -75,7 +75,6 @@ def device_state_attributes(self): @property def state(self): - """Return the state of the sensor.""" return self._state def update(self): diff --git a/custom_components/tapo_control/const.py b/custom_components/tapo_control/const.py index 97a0f53..8f38696 100644 --- a/custom_components/tapo_control/const.py +++ b/custom_components/tapo_control/const.py @@ -2,7 +2,7 @@ from homeassistant.helpers import config_validation as cv -DOMAIN = "tapo_control" +DOMAIN = "tapo" ALARM_MODE = "alarm_mode" PRESET = "preset" LIGHT = "light" diff --git a/custom_components/tapo_control/manifest.json b/custom_components/tapo_control/manifest.json index 7a0a85c..8c2e4e8 100644 --- a/custom_components/tapo_control/manifest.json +++ b/custom_components/tapo_control/manifest.json @@ -1,6 +1,6 @@ { - "domain": "tapo_control", - "name": "Tapo: Cameras Control", + "domain": "tapo", + "name": "Tapo: Cameras", "documentation": "https://github.com/JurajNyiri/HomeAssistant-Tapo-Control", "dependencies": [], "issue_tracker": "https://github.com/JurajNyiri/HomeAssistant-Tapo-Control/issues", diff --git a/custom_components/tapo_control/services.yaml b/custom_components/tapo_control/services.yaml index f4427fd..2951f54 100644 --- a/custom_components/tapo_control/services.yaml +++ b/custom_components/tapo_control/services.yaml @@ -3,7 +3,7 @@ ptz: fields: entity_id: description: "Entity to adjust" - example: "tapo_control.living_room" + example: "tapo.living_room" tilt: description: "Tilt direction. Allowed values: UP, DOWN" example: "UP" @@ -21,7 +21,7 @@ set_privacy_mode: fields: entity_id: description: "Entity to set privacy mode for" - example: "tapo_control.living_room" + example: "tapo.living_room" privacy_mode: description: "Sets privacy mode for camera. Possible values: on, off" example: "on" @@ -30,7 +30,7 @@ set_alarm_mode: fields: entity_id: description: "Entity to set alarm mode for" - example: "tapo_control.living_room" + example: "tapo.living_room" alarm_mode: description: "Sets alarm mode for camera. Possible values: on, off" example: "on" @@ -45,7 +45,7 @@ set_led_mode: fields: entity_id: description: "Entity to set LED mode for" - example: "tapo_control.living_room" + example: "tapo.living_room" led_mode: description: "Sets LED mode for camera. Possible values: on, off" example: "on" @@ -54,7 +54,7 @@ set_motion_detection_mode: fields: entity_id: description: "Entity to set motion detection mode for" - example: "tapo_control.living_room" + example: "tapo.living_room" motion_detection_mode: description: "Sets motion detection mode for camera. Possible values: high, normal, low, off" example: "normal" @@ -65,7 +65,7 @@ set_auto_track_mode: fields: entity_id: description: "Entity to set auto track mode for" - example: "tapo_control.living_room" + example: "tapo.living_room" auto_track_mode: description: "Sets auto track mode for camera. Possible values: on, off" example: "on" @@ -74,13 +74,13 @@ reboot: fields: entity_id: description: "Entity to reboot" - example: "tapo_control.living_room" + example: "tapo.living_room" save_preset: description: Saves the current PTZ position to a preset fields: entity_id: description: "Entity to save the preset for" - example: "tapo_control.living_room" + example: "tapo.living_room" name: description: "Name of the preset. Cannot be empty or a number" example: "Entry Door" @@ -89,7 +89,7 @@ delete_preset: fields: entity_id: description: "Entity to delete the preset for" - example: "tapo_control.living_room" + example: "tapo.living_room" preset: description: "PTZ preset ID or a Name. See possible presets in entity attributes" example: "1" \ No newline at end of file diff --git a/custom_components/tapo_control/strings.json b/custom_components/tapo_control/strings.json index 12c6304..6ef275b 100644 --- a/custom_components/tapo_control/strings.json +++ b/custom_components/tapo_control/strings.json @@ -1,5 +1,5 @@ { - "title": "Tapo: Cameras Control", + "title": "Tapo: Cameras", "config": { "step": { "user": { diff --git a/custom_components/tapo_control/translations/en.json b/custom_components/tapo_control/translations/en.json index c11a068..ccd1f98 100644 --- a/custom_components/tapo_control/translations/en.json +++ b/custom_components/tapo_control/translations/en.json @@ -1,5 +1,5 @@ { - "title": "Tapo: Cameras Control", + "title": "Tapo: Cameras", "config": { "step": { "user": { diff --git a/hacs.json b/hacs.json index da45fcf..ebf5762 100644 --- a/hacs.json +++ b/hacs.json @@ -1,3 +1,3 @@ { - "name": "Tapo: Cameras Control" + "name": "Tapo: Cameras" } \ No newline at end of file diff --git a/info.md b/info.md index 1a71172..5294d1d 100644 --- a/info.md +++ b/info.md @@ -1,12 +1,5 @@ ## Usage: -Add to configuration.yaml: - -``` -tapo_control: - - host: [IP ADDRESS TO TAPO CAMERA] - username: [USERNAME SET IN ADVANCED OPTIONS IN CAMERA APP] - password: [PASSWORD SET IN ADVANCED OPTIONS IN CAMERA APP] -``` +After installation add cameras via Integrations (search for Tapo) in Home Assistant UI For further information see [Github repository](https://github.com/JurajNyiri/HomeAssistant-Tapo-Control/blob/master/README.md) From 5125aa6f7679aa283d1a67c63449cc53d57f3391 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Mon, 5 Oct 2020 16:43:38 +0200 Subject: [PATCH 19/30] Add: Devices --- custom_components/tapo_control/camera.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/custom_components/tapo_control/camera.py b/custom_components/tapo_control/camera.py index 8769791..2bbd14d 100644 --- a/custom_components/tapo_control/camera.py +++ b/custom_components/tapo_control/camera.py @@ -63,7 +63,7 @@ def icon(self) -> str: @property def name(self) -> str: - return f"{self._name}" + return self._name @property def unique_id(self) -> str: @@ -77,6 +77,17 @@ def device_state_attributes(self): def state(self): return self._state + @property + def device_info(self): + return { + "identifiers": {(DOMAIN, slugify(f"{self._mac}_tapo"))}, + "name": self._name, + "manufacturer": "TP-Link", + "model": self._basic_info['device_model'], + "sw_version": self._basic_info['sw_version'] + } + + def update(self): self.manualUpdate() From 9356f2ccf10e78d9fa5ccadf61d8a84a8d3f4a02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Mon, 5 Oct 2020 16:44:42 +0200 Subject: [PATCH 20/30] Revert "Update: Change name to Tapo: Cameras" This reverts commit 760f0ee79825969c9a8eafe185fdc7a42840a250. --- custom_components/tapo_control/__init__.py | 6 +++--- custom_components/tapo_control/camera.py | 3 ++- custom_components/tapo_control/const.py | 2 +- custom_components/tapo_control/manifest.json | 4 ++-- custom_components/tapo_control/services.yaml | 18 +++++++++--------- custom_components/tapo_control/strings.json | 2 +- .../tapo_control/translations/en.json | 2 +- hacs.json | 2 +- info.md | 9 ++++++++- 9 files changed, 28 insertions(+), 20 deletions(-) diff --git a/custom_components/tapo_control/__init__.py b/custom_components/tapo_control/__init__.py index 977e6b6..e3a09f2 100644 --- a/custom_components/tapo_control/__init__.py +++ b/custom_components/tapo_control/__init__.py @@ -10,12 +10,12 @@ _LOGGER = logging.getLogger(__name__) async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Tapo: Cameras component from YAML.""" + """Set up the Tapo: Cameras Control component from YAML.""" return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): - """Set up the Tapo: Cameras component from a config entry.""" + """Set up the Tapo: Cameras Control component from a config entry.""" hass.data.setdefault(DOMAIN, {}) host = entry.data.get(CONF_IP_ADDRESS) @@ -31,7 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) except Exception as e: - _LOGGER.error("Unable to connect to Tapo: Cameras controller: %s", str(e)) + _LOGGER.error("Unable to connect to Tapo: Cameras Control controller: %s", str(e)) raise ConfigEntryNotReady return True \ No newline at end of file diff --git a/custom_components/tapo_control/camera.py b/custom_components/tapo_control/camera.py index 2bbd14d..4287652 100644 --- a/custom_components/tapo_control/camera.py +++ b/custom_components/tapo_control/camera.py @@ -67,7 +67,7 @@ def name(self) -> str: @property def unique_id(self) -> str: - return slugify(f"{self._mac}_tapo") + return slugify(f"{self._mac}_tapo_control") @property def device_state_attributes(self): @@ -75,6 +75,7 @@ def device_state_attributes(self): @property def state(self): + """Return the state of the sensor.""" return self._state @property diff --git a/custom_components/tapo_control/const.py b/custom_components/tapo_control/const.py index 8f38696..97a0f53 100644 --- a/custom_components/tapo_control/const.py +++ b/custom_components/tapo_control/const.py @@ -2,7 +2,7 @@ from homeassistant.helpers import config_validation as cv -DOMAIN = "tapo" +DOMAIN = "tapo_control" ALARM_MODE = "alarm_mode" PRESET = "preset" LIGHT = "light" diff --git a/custom_components/tapo_control/manifest.json b/custom_components/tapo_control/manifest.json index 8c2e4e8..7a0a85c 100644 --- a/custom_components/tapo_control/manifest.json +++ b/custom_components/tapo_control/manifest.json @@ -1,6 +1,6 @@ { - "domain": "tapo", - "name": "Tapo: Cameras", + "domain": "tapo_control", + "name": "Tapo: Cameras Control", "documentation": "https://github.com/JurajNyiri/HomeAssistant-Tapo-Control", "dependencies": [], "issue_tracker": "https://github.com/JurajNyiri/HomeAssistant-Tapo-Control/issues", diff --git a/custom_components/tapo_control/services.yaml b/custom_components/tapo_control/services.yaml index 2951f54..f4427fd 100644 --- a/custom_components/tapo_control/services.yaml +++ b/custom_components/tapo_control/services.yaml @@ -3,7 +3,7 @@ ptz: fields: entity_id: description: "Entity to adjust" - example: "tapo.living_room" + example: "tapo_control.living_room" tilt: description: "Tilt direction. Allowed values: UP, DOWN" example: "UP" @@ -21,7 +21,7 @@ set_privacy_mode: fields: entity_id: description: "Entity to set privacy mode for" - example: "tapo.living_room" + example: "tapo_control.living_room" privacy_mode: description: "Sets privacy mode for camera. Possible values: on, off" example: "on" @@ -30,7 +30,7 @@ set_alarm_mode: fields: entity_id: description: "Entity to set alarm mode for" - example: "tapo.living_room" + example: "tapo_control.living_room" alarm_mode: description: "Sets alarm mode for camera. Possible values: on, off" example: "on" @@ -45,7 +45,7 @@ set_led_mode: fields: entity_id: description: "Entity to set LED mode for" - example: "tapo.living_room" + example: "tapo_control.living_room" led_mode: description: "Sets LED mode for camera. Possible values: on, off" example: "on" @@ -54,7 +54,7 @@ set_motion_detection_mode: fields: entity_id: description: "Entity to set motion detection mode for" - example: "tapo.living_room" + example: "tapo_control.living_room" motion_detection_mode: description: "Sets motion detection mode for camera. Possible values: high, normal, low, off" example: "normal" @@ -65,7 +65,7 @@ set_auto_track_mode: fields: entity_id: description: "Entity to set auto track mode for" - example: "tapo.living_room" + example: "tapo_control.living_room" auto_track_mode: description: "Sets auto track mode for camera. Possible values: on, off" example: "on" @@ -74,13 +74,13 @@ reboot: fields: entity_id: description: "Entity to reboot" - example: "tapo.living_room" + example: "tapo_control.living_room" save_preset: description: Saves the current PTZ position to a preset fields: entity_id: description: "Entity to save the preset for" - example: "tapo.living_room" + example: "tapo_control.living_room" name: description: "Name of the preset. Cannot be empty or a number" example: "Entry Door" @@ -89,7 +89,7 @@ delete_preset: fields: entity_id: description: "Entity to delete the preset for" - example: "tapo.living_room" + example: "tapo_control.living_room" preset: description: "PTZ preset ID or a Name. See possible presets in entity attributes" example: "1" \ No newline at end of file diff --git a/custom_components/tapo_control/strings.json b/custom_components/tapo_control/strings.json index 6ef275b..12c6304 100644 --- a/custom_components/tapo_control/strings.json +++ b/custom_components/tapo_control/strings.json @@ -1,5 +1,5 @@ { - "title": "Tapo: Cameras", + "title": "Tapo: Cameras Control", "config": { "step": { "user": { diff --git a/custom_components/tapo_control/translations/en.json b/custom_components/tapo_control/translations/en.json index ccd1f98..c11a068 100644 --- a/custom_components/tapo_control/translations/en.json +++ b/custom_components/tapo_control/translations/en.json @@ -1,5 +1,5 @@ { - "title": "Tapo: Cameras", + "title": "Tapo: Cameras Control", "config": { "step": { "user": { diff --git a/hacs.json b/hacs.json index ebf5762..da45fcf 100644 --- a/hacs.json +++ b/hacs.json @@ -1,3 +1,3 @@ { - "name": "Tapo: Cameras" + "name": "Tapo: Cameras Control" } \ No newline at end of file diff --git a/info.md b/info.md index 5294d1d..1a71172 100644 --- a/info.md +++ b/info.md @@ -1,5 +1,12 @@ ## Usage: -After installation add cameras via Integrations (search for Tapo) in Home Assistant UI +Add to configuration.yaml: + +``` +tapo_control: + - host: [IP ADDRESS TO TAPO CAMERA] + username: [USERNAME SET IN ADVANCED OPTIONS IN CAMERA APP] + password: [PASSWORD SET IN ADVANCED OPTIONS IN CAMERA APP] +``` For further information see [Github repository](https://github.com/JurajNyiri/HomeAssistant-Tapo-Control/blob/master/README.md) From 9022a00327fbafcbf1a4450bf31bf107dc36ad11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Mon, 5 Oct 2020 16:56:03 +0200 Subject: [PATCH 21/30] Update: Readme --- README.md | 146 +++++++++++++++++++++++++++++++++++++++++++++++++++++- info.md | 9 +--- 2 files changed, 146 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 6460723..9409e9d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,147 @@ # WORK IN PROGRESS -Do not use, this branch is unstable and possibly does not work. \ No newline at end of file +Do not use, this branch is unstable and possibly does not work. + +# HomeAssistant - Tapo: Cameras Control + +Custom component - Tapo: Cameras Control - to control Tapo camera features + +## Installation + +Copy contents of custom_components/tapo_control/ to custom_components/tapo_control/ in your Home Assistant config folder. + +## Installation using HACS + +HACS is a community store for Home Assistant. You can install [HACS](https://github.com/custom-components/hacs) and then install Tapo: Camera Control from the HACS store. + +## Usage + +Add cameras via Integrations (search for Tapo) in Home Assistant UI. + +To add multiple cameras, add integration multiple times. + +## Services + +This custom component creates tapo_control.* entities in your Home Assistant. Use these entity_id(s) in following service calls. + +
+ tapo_control.ptz + + Pan and tilt camera. + + You are also able to use presets and set distance the ptz should travel. + + - **entity_id** Required: Entity to adjust + - **tilt** Optional: Tilt direction. Allowed values: UP, DOWN + - **pan** Optional: Pan direction. Allowed values: RIGHT, LEFT + - **preset** Optional: PTZ preset ID or a Name. See possible presets in entity attributes. + - **distance** Optional: Distance coefficient. Sets how much PTZ should be executed in one request. Allowed values: floating point numbers, 0 to 1 +
+ +
+ tapo_control.set_privacy_mode + + Sets privacy mode. + + If privacy mode is turned on, camera does not record anything and does not respond to anything other than turning off privacy mode. + + - **entity_id** Required: Entity to set privacy mode for + - **privacy_mode** Required: Sets privacy mode for camera. Possible values: on, off +
+ +
+ tapo_control.set_alarm_mode + + Sets alarm mode. + + If camera detects motion, it will sound an alarm, blink the LED or both. + + - **entity_id** Required: Entity to set alarm mode for + - **alarm_mode** Required: Sets alarm mode for camera. Possible values: on, off + - **sound** Optional: Sets whether the alarm should use sound on motion detected. Possible values: on, off + - **light** Optional: Sets whether the alarm should use light on motion detected. Possible values: on, off +
+ +
+ tapo_control.set_led_mode + + Sets LED mode. + + When on, LED is turned on when camera is on. + + When off, LED is always off. + + - **entity_id** Required: Entity to set LED mode for + - **led_mode** Required: Sets LED mode for camera. Possible values: on, off +
+ +
+ tapo_control.set_motion_detection_mode + + Sets motion detection mode. + + Ability to set "high", "normal" or "low". + + These turn on motion detection and set sensitivity to corresponding values in the app. + + Also ability to set to "off", this turns off motion detection completely. + + Turning motion detection off does not affect settings for recordings so you do not need to re-set those unless you open the settings through the Tapo app. + + Notice: If you use motion detection triggered recording and you turn off motion recording, it will no longer record! + + - **entity_id** Required: Entity to set motion detection mode for + - **motion_detection_mode** Required: Sets motion detection mode for camera. Possible values: high, normal, low, off +
+ +
+ tapo_control.set_auto_track_mode + + **Warning: This mode is not available in Tapo app and we do not know why. Use at your own risk and please report any success or failures in [Home Assistant: Community Forum](https://community.home-assistant.io/t/tapo-cameras-control/231795).** + + Sets auto track mode. + + With this mode, camera will be adjusting ptz to track whatever moving object it sees. + + Motion detection setting does not affect this mode. + + - **entity_id** Required: Entity to set auto track mode for + - **auto_track_mode** Required: Sets auto track mode for camera. Possible values: on, off +
+ +
+ tapo_control.reboot + + Reboots the camera + + - **entity_id** Required: Entity to reboot +
+ +
+ tapo_control.save_preset + + Saves the current PTZ position to a preset + + - **entity_id** Required: Entity to save the preset for + - **name** Required: Name of the preset. Cannot be empty or a number +
+ +
+ tapo_control.delete_preset + + Deletes a preset + + - **entity_id** Required: Entity to delete the preset for + - **preset** Required: PTZ preset ID or a Name. See possible presets in entity attributes +
+ +## Have a comment or a suggestion? + +Please [open a new issue](https://github.com/JurajNyiri/HomeAssistant-Tapo-Control/issues/new), or discuss on [Home Assistant: Community Forum](https://community.home-assistant.io/t/tapo-cameras-control/231795). + +## Thank you + +- [Dale Pavey](https://research.nccgroup.com/2020/07/31/lights-camera-hacked-an-insight-into-the-world-of-popular-ip-cameras/) from NCC Group for the initial research on the Tapo C200 +- [likaci](https://github.com/likaci) and [his github repository](https://github.com/likaci/mercury-ipc-control) for the research on the Mercury camera on which tapo is based +- [Tim Zhang](https://github.com/ttimasdf) for additional research for Mercury camera on [his github repository](https://github.com/ttimasdf/mercury-ipc-control) +- [Gábor Szabados](https://github.com/GSzabados) for doing research and gathering all the information above in [Home Assistant Community forum](https://community.home-assistant.io/t/use-pan-tilt-function-for-tp-link-tapo-c200-from-home-assistant/170143/18) \ No newline at end of file diff --git a/info.md b/info.md index 1a71172..6967ea7 100644 --- a/info.md +++ b/info.md @@ -1,12 +1,5 @@ ## Usage: -Add to configuration.yaml: - -``` -tapo_control: - - host: [IP ADDRESS TO TAPO CAMERA] - username: [USERNAME SET IN ADVANCED OPTIONS IN CAMERA APP] - password: [PASSWORD SET IN ADVANCED OPTIONS IN CAMERA APP] -``` +After installation add cameras via Integrations (search for Tapo) in Home Assistant UI. For further information see [Github repository](https://github.com/JurajNyiri/HomeAssistant-Tapo-Control/blob/master/README.md) From 64bd652fb23fd95389e96f90735310f082eb42ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Mon, 5 Oct 2020 17:07:51 +0200 Subject: [PATCH 22/30] Add: format --- custom_components/tapo_control/__init__.py | 1 - custom_components/tapo_control/camera.py | 7 ++- custom_components/tapo_control/const.py | 5 +++ custom_components/tapo_control/services.yaml | 26 ++++++----- custom_components/tapo_control/utils.py | 45 -------------------- 5 files changed, 27 insertions(+), 57 deletions(-) delete mode 100644 custom_components/tapo_control/utils.py diff --git a/custom_components/tapo_control/__init__.py b/custom_components/tapo_control/__init__.py index e3a09f2..b8958ed 100644 --- a/custom_components/tapo_control/__init__.py +++ b/custom_components/tapo_control/__init__.py @@ -5,7 +5,6 @@ from homeassistant.exceptions import ConfigEntryNotReady import logging from .const import * -from .utils import * _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/tapo_control/camera.py b/custom_components/tapo_control/camera.py index 4287652..3936ebf 100644 --- a/custom_components/tapo_control/camera.py +++ b/custom_components/tapo_control/camera.py @@ -1,5 +1,4 @@ from .const import * -from .utils import * from homeassistant.core import HomeAssistant from typing import Callable from homeassistant.helpers.entity import Entity @@ -41,6 +40,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: dict, async_add_entities platform.async_register_entity_service( SERVICE_DELETE_PRESET, SCHEMA_SERVICE_DELETE_PRESET, "delete_preset", ) + platform.async_register_entity_service( + SERVICE_FORMAT, SCHEMA_SERVICE_FORMAT, "format", + ) class TapoCam(Entity): @@ -202,4 +204,7 @@ def delete_preset(self, preset): self.manualUpdate() else: _LOGGER.error("Preset "+preset+" does not exist.") + + def format(self): + self._controller.format() \ No newline at end of file diff --git a/custom_components/tapo_control/const.py b/custom_components/tapo_control/const.py index 97a0f53..d01452d 100644 --- a/custom_components/tapo_control/const.py +++ b/custom_components/tapo_control/const.py @@ -80,4 +80,9 @@ SCHEMA_SERVICE_DELETE_PRESET = { vol.Required(ENTITY_ID): cv.string, vol.Required(PRESET): cv.string +} + +SERVICE_FORMAT = "format" +SCHEMA_SERVICE_FORMAT = { + vol.Required(ENTITY_ID): cv.string } \ No newline at end of file diff --git a/custom_components/tapo_control/services.yaml b/custom_components/tapo_control/services.yaml index f4427fd..971672b 100644 --- a/custom_components/tapo_control/services.yaml +++ b/custom_components/tapo_control/services.yaml @@ -3,7 +3,7 @@ ptz: fields: entity_id: description: "Entity to adjust" - example: "tapo_control.living_room" + example: "camera.living_room" tilt: description: "Tilt direction. Allowed values: UP, DOWN" example: "UP" @@ -21,7 +21,7 @@ set_privacy_mode: fields: entity_id: description: "Entity to set privacy mode for" - example: "tapo_control.living_room" + example: "camera.living_room" privacy_mode: description: "Sets privacy mode for camera. Possible values: on, off" example: "on" @@ -30,7 +30,7 @@ set_alarm_mode: fields: entity_id: description: "Entity to set alarm mode for" - example: "tapo_control.living_room" + example: "camera.living_room" alarm_mode: description: "Sets alarm mode for camera. Possible values: on, off" example: "on" @@ -45,7 +45,7 @@ set_led_mode: fields: entity_id: description: "Entity to set LED mode for" - example: "tapo_control.living_room" + example: "camera.living_room" led_mode: description: "Sets LED mode for camera. Possible values: on, off" example: "on" @@ -54,7 +54,7 @@ set_motion_detection_mode: fields: entity_id: description: "Entity to set motion detection mode for" - example: "tapo_control.living_room" + example: "camera.living_room" motion_detection_mode: description: "Sets motion detection mode for camera. Possible values: high, normal, low, off" example: "normal" @@ -65,7 +65,7 @@ set_auto_track_mode: fields: entity_id: description: "Entity to set auto track mode for" - example: "tapo_control.living_room" + example: "camera.living_room" auto_track_mode: description: "Sets auto track mode for camera. Possible values: on, off" example: "on" @@ -74,13 +74,13 @@ reboot: fields: entity_id: description: "Entity to reboot" - example: "tapo_control.living_room" + example: "camera.living_room" save_preset: description: Saves the current PTZ position to a preset fields: entity_id: description: "Entity to save the preset for" - example: "tapo_control.living_room" + example: "camera.living_room" name: description: "Name of the preset. Cannot be empty or a number" example: "Entry Door" @@ -89,7 +89,13 @@ delete_preset: fields: entity_id: description: "Entity to delete the preset for" - example: "tapo_control.living_room" + example: "camera.living_room" preset: description: "PTZ preset ID or a Name. See possible presets in entity attributes" - example: "1" \ No newline at end of file + example: "1" +format: + description: Formats the SD card of a camera + fields: + entity_id: + description: "Entity to format" + example: "camera.living_room" \ No newline at end of file diff --git a/custom_components/tapo_control/utils.py b/custom_components/tapo_control/utils.py deleted file mode 100644 index 300ce67..0000000 --- a/custom_components/tapo_control/utils.py +++ /dev/null @@ -1,45 +0,0 @@ -import re -import unidecode -from .const import * - -def getIncrement(entity_id): - lastNum = entity_id[entity_id.rindex('_')+1:] - if(lastNum.isnumeric()): - return int(lastNum)+1 - return 1 - -def addTapoEntityID(tapo, requested_entity_id, requested_value): - regex = r"^"+requested_entity_id.replace(".","\.")+"_[0-9]+$" - if(requested_entity_id in tapo): - biggestIncrement = 0 - for id in tapo: - r1 = re.findall(regex,id) - if r1: - inc = getIncrement(requested_entity_id) - if(inc > biggestIncrement): - biggestIncrement = inc - if(biggestIncrement == 0): - oldVal = tapo[requested_entity_id] - tapo.pop(requested_entity_id, None) - tapo[requested_entity_id+"_1"] = oldVal - tapo[requested_entity_id+"_2"] = requested_value - else: - tapo[requested_entity_id+"_"+str(biggestIncrement)] = requested_value - else: - biggestIncrement = 0 - for id in tapo: - r1 = re.findall(regex,id) - if r1: - inc = getIncrement(id) - if(inc > biggestIncrement): - biggestIncrement = inc - if(biggestIncrement == 0): - tapo[requested_entity_id] = requested_value - else: - tapo[requested_entity_id+"_"+str(biggestIncrement)] = requested_value - return tapo - -def generateEntityIDFromName(name): - str = unidecode.unidecode(name.rstrip().replace(".","_").replace(" ", "_").lower()) - str = re.sub("_"+'{2,}',"_",''.join(filter(ENTITY_CHAR_WHITELIST.__contains__, str))) - return DOMAIN+"."+str \ No newline at end of file From 012be27a03dcb346269ad64f2f290949c282194a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Mon, 5 Oct 2020 17:10:54 +0200 Subject: [PATCH 23/30] Update: Readme information for format service --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 9409e9d..2894237 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,14 @@ This custom component creates tapo_control.* entities in your Home Assistant. Us - **led_mode** Required: Sets LED mode for camera. Possible values: on, off +
+ tapo_control.format + + Formats SD card of a camera + + - **entity_id** Required: Entity to format +
+
tapo_control.set_motion_detection_mode From 29c73dafbcdd79700480f5b986a2cb7dbc254774 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Mon, 5 Oct 2020 18:07:33 +0200 Subject: [PATCH 24/30] Add: Camera stream --- custom_components/tapo_control/camera.py | 72 +++++++++++++++++++++--- 1 file changed, 63 insertions(+), 9 deletions(-) diff --git a/custom_components/tapo_control/camera.py b/custom_components/tapo_control/camera.py index 3936ebf..bfc97d8 100644 --- a/custom_components/tapo_control/camera.py +++ b/custom_components/tapo_control/camera.py @@ -1,16 +1,20 @@ +from homeassistant.helpers.config_validation import boolean from .const import * from homeassistant.core import HomeAssistant +from homeassistant.const import (CONF_IP_ADDRESS, CONF_USERNAME, CONF_PASSWORD) from typing import Callable from homeassistant.helpers.entity import Entity from pytapo import Tapo from homeassistant.util import slugify from homeassistant.helpers import entity_platform +from homeassistant.components.camera import SUPPORT_STREAM, Camera import logging _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: dict, async_add_entities: Callable): - async_add_entities([TapoCam(entry, hass.data[DOMAIN][entry.entry_id])]) + async_add_entities([TapoCam(entry, hass.data[DOMAIN][entry.entry_id],True)]) + async_add_entities([TapoCam(entry, hass.data[DOMAIN][entry.entry_id],False)]) platform = entity_platform.current_platform.get() platform.async_register_entity_service( @@ -45,31 +49,41 @@ async def async_setup_entry(hass: HomeAssistant, entry: dict, async_add_entities ) -class TapoCam(Entity): - def __init__(self, entry: dict, controller: Tapo): +class TapoCam(Camera): + def __init__(self, entry: dict, controller: Tapo, HDStream: boolean): + super().__init__() self._basic_info = controller.getBasicInfo()['device_info']['basic_info'] - self._name = self._basic_info['device_alias'] self._mac = self._basic_info['mac'] self._controller = controller self._attributes = self._basic_info self._entry = entry + self._hdstream = HDStream self._state = "idle" + self._host = entry.data.get(CONF_IP_ADDRESS) + self._username = entry.data.get(CONF_USERNAME) + self._password = entry.data.get(CONF_PASSWORD) + + self.access_tokens = [controller.stok] #todo: changeme if(self._basic_info['device_model'] in DEVICES_WITH_NO_PRESETS): self._attributes['presets'] = {} else: self._attributes['presets'] = self._controller.getPresets() + @property + def supported_features(self): + return SUPPORT_STREAM + @property def icon(self) -> str: return "mdi:cctv" @property def name(self) -> str: - return self._name + return self.getName() @property def unique_id(self) -> str: - return slugify(f"{self._mac}_tapo_control") + return self.getUniqueID() @property def device_state_attributes(self): @@ -83,20 +97,44 @@ def state(self): @property def device_info(self): return { - "identifiers": {(DOMAIN, slugify(f"{self._mac}_tapo"))}, - "name": self._name, + "identifiers": {(DOMAIN, self.getUniqueID())}, + "name": self.getName(), "manufacturer": "TP-Link", "model": self._basic_info['device_model'], "sw_version": self._basic_info['sw_version'] } + @property + def is_recording(self): + """TODO""" + return True + + @property + def motion_detection_enabled(self): + """TODO""" + return True + + @property + def brand(self): + return "TP-Link" + + @property + def model(self): + return self._basic_info['device_model'] + + async def stream_source(self): + if(self._hdstream): + streamType = "stream1" + else: + streamType = "stream2" + streamURL = f"rtsp://{self._username}:{self._password}@{self._host}:554/{streamType}" + return streamURL def update(self): self.manualUpdate() def manualUpdate(self): self._basic_info = self._controller.getBasicInfo()['device_info']['basic_info'] - self._name = self._basic_info['device_alias'] self._attributes = self._basic_info self._state = "idle" if(self._basic_info['device_model'] in DEVICES_WITH_NO_PRESETS): @@ -104,6 +142,22 @@ def manualUpdate(self): else: self._attributes['presets'] = self._controller.getPresets() + def getName(self): + name = self._basic_info['device_alias'] + if(self._hdstream): + name += " - HD" + else: + name += " - SD" + return name + + def getUniqueID(self): + if(self._hdstream): + streamType = "hd" + else: + streamType = "sd" + return slugify(f"{self._mac}_{streamType}_tapo_control") + + def ptz(self, tilt = None, pan = None, preset = None, distance = None): if preset: if(preset.isnumeric()): From 1e2a1e846994388aeeed180457912549e325dd62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Mon, 5 Oct 2020 18:16:53 +0200 Subject: [PATCH 25/30] Add: Each camera now creates 2 entities for different stream qualities --- custom_components/tapo_control/camera.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/custom_components/tapo_control/camera.py b/custom_components/tapo_control/camera.py index bfc97d8..a4ab37b 100644 --- a/custom_components/tapo_control/camera.py +++ b/custom_components/tapo_control/camera.py @@ -13,8 +13,8 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: dict, async_add_entities: Callable): - async_add_entities([TapoCam(entry, hass.data[DOMAIN][entry.entry_id],True)]) - async_add_entities([TapoCam(entry, hass.data[DOMAIN][entry.entry_id],False)]) + async_add_entities([TapoCamEntity(entry, hass.data[DOMAIN][entry.entry_id],True)]) + async_add_entities([TapoCamEntity(entry, hass.data[DOMAIN][entry.entry_id],False)]) platform = entity_platform.current_platform.get() platform.async_register_entity_service( @@ -49,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: dict, async_add_entities ) -class TapoCam(Camera): +class TapoCamEntity(Camera): def __init__(self, entry: dict, controller: Tapo, HDStream: boolean): super().__init__() self._basic_info = controller.getBasicInfo()['device_info']['basic_info'] @@ -63,7 +63,6 @@ def __init__(self, entry: dict, controller: Tapo, HDStream: boolean): self._username = entry.data.get(CONF_USERNAME) self._password = entry.data.get(CONF_PASSWORD) - self.access_tokens = [controller.stok] #todo: changeme if(self._basic_info['device_model'] in DEVICES_WITH_NO_PRESETS): self._attributes['presets'] = {} else: @@ -97,8 +96,8 @@ def state(self): @property def device_info(self): return { - "identifiers": {(DOMAIN, self.getUniqueID())}, - "name": self.getName(), + "identifiers": {(DOMAIN, slugify(f"{self._mac}_tapo_control"))}, + "name": self._basic_info['device_alias'], "manufacturer": "TP-Link", "model": self._basic_info['device_model'], "sw_version": self._basic_info['sw_version'] From 8e4f52136dd90f32a9a254b9c38cf0ef811eda3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Mon, 5 Oct 2020 20:14:28 +0200 Subject: [PATCH 26/30] Add: motion_detection and motion_detection_sensitivity attributes --- custom_components/tapo_control/camera.py | 45 +++++++++++++++----- custom_components/tapo_control/const.py | 3 +- custom_components/tapo_control/manifest.json | 2 +- 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/custom_components/tapo_control/camera.py b/custom_components/tapo_control/camera.py index a4ab37b..e07cb42 100644 --- a/custom_components/tapo_control/camera.py +++ b/custom_components/tapo_control/camera.py @@ -52,21 +52,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: dict, async_add_entities class TapoCamEntity(Camera): def __init__(self, entry: dict, controller: Tapo, HDStream: boolean): super().__init__() - self._basic_info = controller.getBasicInfo()['device_info']['basic_info'] - self._mac = self._basic_info['mac'] + self._attributes = {} + self._motion_detection_enabled = None + self._motion_detection_sensitivity = None + self._basic_info = {} + self._mac = "" + self._controller = controller - self._attributes = self._basic_info self._entry = entry self._hdstream = HDStream - self._state = "idle" self._host = entry.data.get(CONF_IP_ADDRESS) self._username = entry.data.get(CONF_USERNAME) self._password = entry.data.get(CONF_PASSWORD) - - if(self._basic_info['device_model'] in DEVICES_WITH_NO_PRESETS): - self._attributes['presets'] = {} - else: - self._attributes['presets'] = self._controller.getPresets() + self.manualUpdate() @property def supported_features(self): @@ -110,8 +108,7 @@ def is_recording(self): @property def motion_detection_enabled(self): - """TODO""" - return True + return self._motion_detection_enabled @property def brand(self): @@ -120,6 +117,10 @@ def brand(self): @property def model(self): return self._basic_info['device_model'] + + @property + def should_poll(self): + return True async def stream_source(self): if(self._hdstream): @@ -135,7 +136,24 @@ def update(self): def manualUpdate(self): self._basic_info = self._controller.getBasicInfo()['device_info']['basic_info'] self._attributes = self._basic_info + self._mac = self._basic_info['mac'] self._state = "idle" + try: + motionDetectionData = self._controller.getMotionDetection() + self._motion_detection_enabled = motionDetectionData['enabled'] + if(motionDetectionData['digital_sensitivity'] == "20"): + self._motion_detection_sensitivity = "low" + elif(motionDetectionData['digital_sensitivity'] == "50"): + self._motion_detection_sensitivity = "normal" + elif(motionDetectionData['digital_sensitivity'] == "80"): + self._motion_detection_sensitivity = "high" + else: + self._motion_detection_sensitivity = None + except: + self._motion_detection_enabled = None + self._motion_detection_sensitivity = None + self._attributes['motion_detection_sensitivity'] = self._motion_detection_sensitivity + if(self._basic_info['device_model'] in DEVICES_WITH_NO_PRESETS): self._attributes['presets'] = {} else: @@ -204,6 +222,7 @@ def set_privacy_mode(self, privacy_mode: str): self._controller.setPrivacyMode(True) else: self._controller.setPrivacyMode(False) + self.manualUpdate() def set_alarm_mode(self, alarm_mode, sound = None, light = None): if(not light): @@ -214,24 +233,28 @@ def set_alarm_mode(self, alarm_mode, sound = None, light = None): self._controller.setAlarm(True, True if sound == "on" else False, True if light == "on" else False) else: self._controller.setAlarm(False, True if sound == "on" else False, True if light == "on" else False) + self.manualUpdate() def set_led_mode(self, led_mode: str): if(led_mode == "on"): self._controller.setLEDEnabled(True) else: self._controller.setLEDEnabled(False) + self.manualUpdate() def set_motion_detection_mode(self, motion_detection_mode): if(motion_detection_mode == "off"): self._controller.setMotionDetection(False) else: self._controller.setMotionDetection(True, motion_detection_mode) + self.manualUpdate() def set_auto_track_mode(self, auto_track_mode: str): if(auto_track_mode == "on"): self._controller.setAutoTrackTarget(True) else: self._controller.setAutoTrackTarget(False) + self.manualUpdate() def reboot(self): self._controller.reboot() diff --git a/custom_components/tapo_control/const.py b/custom_components/tapo_control/const.py index d01452d..a4d1801 100644 --- a/custom_components/tapo_control/const.py +++ b/custom_components/tapo_control/const.py @@ -1,5 +1,5 @@ import voluptuous as vol - +from datetime import timedelta from homeassistant.helpers import config_validation as cv DOMAIN = "tapo_control" @@ -21,6 +21,7 @@ DEVICE_MODEL_C100 = "C100" DEVICE_MODEL_C200 = "C200" DEVICES_WITH_NO_PRESETS = [DEVICE_MODEL_C100] +SCAN_INTERVAL = timedelta(seconds=30) TOGGLE_STATES = ["on", "off"] diff --git a/custom_components/tapo_control/manifest.json b/custom_components/tapo_control/manifest.json index 7a0a85c..d415b65 100644 --- a/custom_components/tapo_control/manifest.json +++ b/custom_components/tapo_control/manifest.json @@ -5,7 +5,7 @@ "dependencies": [], "issue_tracker": "https://github.com/JurajNyiri/HomeAssistant-Tapo-Control/issues", "codeowners": ["@JurajNyiri"], - "requirements": ["pytapo==0.10"], + "requirements": ["pytapo==0.11"], "config_flow": true } \ No newline at end of file From 72277d00f9bdf1ec01ed4498da44034a6e4f9c44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Mon, 5 Oct 2020 20:19:09 +0200 Subject: [PATCH 27/30] Add: privacy_mode attribute --- custom_components/tapo_control/camera.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/custom_components/tapo_control/camera.py b/custom_components/tapo_control/camera.py index e07cb42..e8f036b 100644 --- a/custom_components/tapo_control/camera.py +++ b/custom_components/tapo_control/camera.py @@ -55,6 +55,7 @@ def __init__(self, entry: dict, controller: Tapo, HDStream: boolean): self._attributes = {} self._motion_detection_enabled = None self._motion_detection_sensitivity = None + self._privacy_mode = None self._basic_info = {} self._mac = "" @@ -154,6 +155,14 @@ def manualUpdate(self): self._motion_detection_sensitivity = None self._attributes['motion_detection_sensitivity'] = self._motion_detection_sensitivity + try: + self._privacy_mode = self._controller.getPrivacyMode()['enabled'] + except: + self._privacy_mode = None + self._attributes['privacy_mode'] = self._privacy_mode + + + if(self._basic_info['device_model'] in DEVICES_WITH_NO_PRESETS): self._attributes['presets'] = {} else: From ee236e51a3b5820d583853a503cb1ebd9959e2a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Mon, 5 Oct 2020 20:27:29 +0200 Subject: [PATCH 28/30] Add: alarm and alarm_mode, led, auto_track attributes --- custom_components/tapo_control/camera.py | 25 ++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/custom_components/tapo_control/camera.py b/custom_components/tapo_control/camera.py index e8f036b..0a1ca3f 100644 --- a/custom_components/tapo_control/camera.py +++ b/custom_components/tapo_control/camera.py @@ -58,6 +58,10 @@ def __init__(self, entry: dict, controller: Tapo, HDStream: boolean): self._privacy_mode = None self._basic_info = {} self._mac = "" + self._alarm = None + self._alarm_mode = None + self._led = None + self._auto_track = None self._controller = controller self._entry = entry @@ -161,6 +165,27 @@ def manualUpdate(self): self._privacy_mode = None self._attributes['privacy_mode'] = self._privacy_mode + try: + alarmData = self._controller.getAlarm() + self._alarm = alarmData['enabled'] + self._alarm_mode = alarmData['alarm_mode'] + except: + self._alarm = None + self._alarm_mode = None + self._attributes['alarm'] = self._alarm + self._attributes['alarm_mode'] = self._alarm_mode + + try: + self._led = self._controller.getLED()['enabled'] + except: + self._led = None + self._attributes['led'] = self._led + + try: + self._auto_track = self._controller.getAutoTrackTarget()['enabled'] + except: + self._auto_track = None + self._attributes['auto_track'] = self._auto_track if(self._basic_info['device_model'] in DEVICES_WITH_NO_PRESETS): From c81b8c65115d3e927640bc3c294374bd07148525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Mon, 5 Oct 2020 20:28:55 +0200 Subject: [PATCH 29/30] Remove: is_recording placeholder --- custom_components/tapo_control/camera.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/custom_components/tapo_control/camera.py b/custom_components/tapo_control/camera.py index 0a1ca3f..d814088 100644 --- a/custom_components/tapo_control/camera.py +++ b/custom_components/tapo_control/camera.py @@ -105,11 +105,6 @@ def device_info(self): "model": self._basic_info['device_model'], "sw_version": self._basic_info['sw_version'] } - - @property - def is_recording(self): - """TODO""" - return True @property def motion_detection_enabled(self): From 1e3125d8308932b77ae88be91a7c054e0d710c10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Mon, 5 Oct 2020 20:29:23 +0200 Subject: [PATCH 30/30] Update: Remove Work In Progress warning from README --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 2894237..56348e6 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,3 @@ -# WORK IN PROGRESS - -Do not use, this branch is unstable and possibly does not work. - # HomeAssistant - Tapo: Cameras Control Custom component - Tapo: Cameras Control - to control Tapo camera features