diff --git a/README.md b/README.md
index 2b05a1d..56348e6 100644
--- a/README.md
+++ b/README.md
@@ -12,16 +12,9 @@ HACS is a community store for Home Assistant. You can install [HACS](https://git
## Usage
-Add to configuration.yaml:
+Add cameras via Integrations (search for Tapo) in Home Assistant UI.
-```
-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.
+To add multiple cameras, add integration multiple times.
## Services
@@ -78,6 +71,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
diff --git a/custom_components/tapo_control/__init__.py b/custom_components/tapo_control/__init__.py
index 8263186..b8958ed 100644
--- a/custom_components/tapo_control/__init__.py
+++ b/custom_components/tapo_control/__init__.py
@@ -1,369 +1,36 @@
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
-import unidecode
-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
-ENTITY_CHAR_WHITELIST = set('abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_')
-DEVICE_MODEL_C100 = "C100"
-DEVICE_MODEL_C200 = "C200"
-DEVICES_WITH_NO_PRESETS = [DEVICE_MODEL_C100]
-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
- }
- )
- ],
- )
- },
- extra=vol.ALLOW_EXTRA,
-)
-
-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 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)
- 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.")
+async def async_setup(hass: HomeAssistant, config: dict):
+ """Set up the Tapo: Cameras Control component from YAML."""
+ return True
- 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.")
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Set up the Tapo: Cameras Control component from a config entry."""
+ hass.data.setdefault(DOMAIN, {})
- 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.")
+ 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)
- 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.")
+ hass.data[DOMAIN][entry.entry_id] = tapoController
- 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.")
-
- 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]
- 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(entity_id,tapoConnector)
-
-
- for entity_id in tapo:
- tapoConnector = tapo[entity_id]
- manualUpdate(entity_id, tapoConnector)
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, "camera")
+ )
- 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)
+ except Exception as e:
+ _LOGGER.error("Unable to connect to Tapo: Cameras Control controller: %s", str(e))
+ raise ConfigEntryNotReady
- track_time_interval(hass, update, timedelta(seconds=DEFAULT_SCAN_INTERVAL))
-
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..d814088
--- /dev/null
+++ b/custom_components/tapo_control/camera.py
@@ -0,0 +1,315 @@
+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([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(
+ 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",
+ )
+ 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",
+ )
+ 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",
+ )
+ 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",
+ )
+ platform.async_register_entity_service(
+ SERVICE_FORMAT, SCHEMA_SERVICE_FORMAT, "format",
+ )
+
+
+class TapoCamEntity(Camera):
+ def __init__(self, entry: dict, controller: Tapo, HDStream: boolean):
+ super().__init__()
+ self._attributes = {}
+ self._motion_detection_enabled = None
+ self._motion_detection_sensitivity = None
+ 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
+ self._hdstream = HDStream
+ self._host = entry.data.get(CONF_IP_ADDRESS)
+ self._username = entry.data.get(CONF_USERNAME)
+ self._password = entry.data.get(CONF_PASSWORD)
+ self.manualUpdate()
+
+ @property
+ def supported_features(self):
+ return SUPPORT_STREAM
+
+ @property
+ def icon(self) -> str:
+ return "mdi:cctv"
+
+ @property
+ def name(self) -> str:
+ return self.getName()
+
+ @property
+ def unique_id(self) -> str:
+ return self.getUniqueID()
+
+ @property
+ def device_state_attributes(self):
+ return self._attributes
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._state
+
+ @property
+ def device_info(self):
+ return {
+ "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']
+ }
+
+ @property
+ def motion_detection_enabled(self):
+ return self._motion_detection_enabled
+
+ @property
+ def brand(self):
+ return "TP-Link"
+
+ @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):
+ 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._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
+
+ try:
+ self._privacy_mode = self._controller.getPrivacyMode()['enabled']
+ except:
+ 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):
+ self._attributes['presets'] = {}
+ 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()):
+ 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 + ".")
+
+ def set_privacy_mode(self, privacy_mode: str):
+ if(privacy_mode == "on"):
+ 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):
+ 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)
+ 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()
+
+ 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.")
+
+ 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.")
+
+ def format(self):
+ self._controller.format()
+
\ No newline at end of file
diff --git a/custom_components/tapo_control/config_flow.py b/custom_components/tapo_control/config_flow.py
new file mode 100644
index 0000000..f2d7bc6
--- /dev/null
+++ b/custom_components/tapo_control/config_flow.py
@@ -0,0 +1,51 @@
+from pytapo import Tapo
+from homeassistant import config_entries
+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, 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=DEVICE_SCHEMA, errors=errors
+ )
\ 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..a4d1801
--- /dev/null
+++ b/custom_components/tapo_control/const.py
@@ -0,0 +1,89 @@
+import voluptuous as vol
+from datetime import timedelta
+from homeassistant.helpers import config_validation as cv
+
+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
+ENTITY_CHAR_WHITELIST = set('abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_')
+DEVICE_MODEL_C100 = "C100"
+DEVICE_MODEL_C200 = "C200"
+DEVICES_WITH_NO_PRESETS = [DEVICE_MODEL_C100]
+SCAN_INTERVAL = timedelta(seconds=30)
+
+TOGGLE_STATES = ["on", "off"]
+
+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_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_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,
+ 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"])
+}
+
+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)
+}
+
+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
+}
+
+SERVICE_DELETE_PRESET = "delete_preset"
+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/manifest.json b/custom_components/tapo_control/manifest.json
index dbfb2d1..d415b65 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.10","unidecode"]
+ "requirements": ["pytapo==0.11"],
+ "config_flow": true
}
\ 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/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
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)