diff --git a/custom_components/hikvision_axpro/__init__.py b/custom_components/hikvision_axpro/__init__.py index 5638521..aa0d142 100644 --- a/custom_components/hikvision_axpro/__init__.py +++ b/custom_components/hikvision_axpro/__init__.py @@ -7,12 +7,20 @@ from homeassistant.components.alarm_control_panel import SCAN_INTERVAL from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD, Platform +from homeassistant.const import ( + ATTR_CODE_FORMAT, + CONF_ENABLED, + CONF_HOST, + CONF_USERNAME, + CONF_PASSWORD, + CONF_CODE, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DATA_COORDINATOR, DOMAIN +from .const import DATA_COORDINATOR, DOMAIN, USE_CODE_ARMING PLATFORMS: list[Platform] = [Platform.ALARM_CONTROL_PANEL] _LOGGER = logging.getLogger(__name__) @@ -23,6 +31,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: host = entry.data[CONF_HOST] username = entry.data[CONF_USERNAME] password = entry.data[CONF_PASSWORD] + use_code = entry.data[CONF_ENABLED] + code_format = entry.data[ATTR_CODE_FORMAT] + code = entry.data[CONF_CODE] + use_code_arming = entry.data[USE_CODE_ARMING] axpro = hikaxpro.HikAxPro(host, username, password) try: @@ -31,7 +43,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except (asyncio.TimeoutError, ConnectionError) as ex: raise ConfigEntryNotReady from ex - coordinator = HikAxProDataUpdateCoordinator(hass, axpro, mac) + coordinator = HikAxProDataUpdateCoordinator( + hass, + axpro, + mac, + use_code, + code_format, + use_code_arming, + code, + ) hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = {DATA_COORDINATOR: coordinator} @@ -51,11 +71,24 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class HikAxProDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching ax pro data.""" - def __init__(self, hass, axpro, mac): + def __init__( + self, + hass, + axpro, + mac, + use_code, + code_format, + use_code_arming, + code, + ): self.axpro = axpro self.state = None self.host = axpro.host self.mac = mac + self.use_code = use_code + self.code_format = code_format + self.use_code_arming = use_code_arming + self.code = code super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) @@ -96,4 +129,4 @@ async def async_disarm(self): if is_success: await self._async_update_data() - await self.async_request_refresh() \ No newline at end of file + await self.async_request_refresh() diff --git a/custom_components/hikvision_axpro/alarm_control_panel.py b/custom_components/hikvision_axpro/alarm_control_panel.py index 6dfa77f..77a2637 100644 --- a/custom_components/hikvision_axpro/alarm_control_panel.py +++ b/custom_components/hikvision_axpro/alarm_control_panel.py @@ -2,12 +2,14 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.components.alarm_control_panel import ( AlarmControlPanelEntity, AlarmControlPanelEntityFeature, + CodeFormat, ) from .const import DATA_COORDINATOR, DOMAIN @@ -58,14 +60,47 @@ def state(self): """Return the state of the device.""" return self.coordinator.state + @property + def code_format(self) -> CodeFormat | None: + """Return the code format.""" + return self.__get_code_format(self.coordinator.code_format) + + def __get_code_format(self, code_format_str) -> CodeFormat: + """Returns CodeFormat according to the given code fomrat string.""" + code_format: CodeFormat = None + + if not self.coordinator.use_code: + code_format = None + elif code_format_str == "NUMBER": + code_format = CodeFormat.NUMBER + elif code_format_str == "TEXT": + code_format = CodeFormat.TEXT + + return code_format + async def async_alarm_disarm(self, code=None): """Send disarm command.""" + if self.coordinator.use_code: + if not self.__is_code_valid(code): + return + await self.coordinator.async_disarm() async def async_alarm_arm_home(self, code=None): """Send arm home command.""" + if self.coordinator.use_code and self.coordinator.use_code_arming: + if not self.__is_code_valid(code): + return + await self.coordinator.async_arm_home() async def async_alarm_arm_away(self, code=None): """Send arm away command.""" + if self.coordinator.use_code and self.coordinator.use_code_arming: + if not self.__is_code_valid(code): + return + await self.coordinator.async_arm_away() + + def __is_code_valid(self, code): + return code == self.coordinator.code diff --git a/custom_components/hikvision_axpro/config_flow.py b/custom_components/hikvision_axpro/config_flow.py index 232f2b7..f06e5e1 100644 --- a/custom_components/hikvision_axpro/config_flow.py +++ b/custom_components/hikvision_axpro/config_flow.py @@ -10,9 +10,16 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError -from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD +from homeassistant.const import ( + CONF_CODE, + CONF_ENABLED, + ATTR_CODE_FORMAT, + CONF_HOST, + CONF_USERNAME, + CONF_PASSWORD, +) -from .const import DOMAIN +from .const import DOMAIN, USE_CODE_ARMING _LOGGER = logging.getLogger(__name__) @@ -21,6 +28,10 @@ vol.Required(CONF_HOST): str, vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_ENABLED, default=False): bool, + vol.Optional(ATTR_CODE_FORMAT, default="NUMBER"): vol.In(["TEXT", "NUMBER"]), + vol.Optional(CONF_CODE, default=""): str, + vol.Optional(USE_CODE_ARMING, default=False): bool, } ) @@ -39,7 +50,7 @@ def __init__( async def authenticate(self) -> bool: """Check the provided credentials by connecting to ax pro.""" - is_connect_success = self.axpro.connect() + is_connect_success = await self.hass.async_add_executor_job(self.axpro.connect) return is_connect_success @@ -49,9 +60,22 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. """ + if data[CONF_ENABLED]: + if data[ATTR_CODE_FORMAT] is None or ( + data[ATTR_CODE_FORMAT] != "NUMBER" and data[ATTR_CODE_FORMAT] != "TEXT" + ): + raise InvalidCodeFormat + + if ( + data[CONF_CODE] is None + or data[CONF_CODE] == "" + or (data[ATTR_CODE_FORMAT] == "NUMBER" and not str.isdigit(data[CONF_CODE])) + ): + raise InvalidCode + hub = AxProHub(data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD], hass) - if not await hass.async_add_executor_job(hub.authenticate): + if not await hub.authenticate(): raise InvalidAuth return {"title": f"Hikvision_axpro_{data['host']}"} @@ -77,6 +101,10 @@ async def async_step_user(self, user_input=None) -> FlowResult: errors["base"] = "cannot_connect" except InvalidAuth: errors["base"] = "invalid_auth" + except InvalidCodeFormat: + errors["base"] = "invalid_code_format" + except InvalidCode: + errors["base"] = "invalid_code" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" @@ -94,3 +122,11 @@ class CannotConnect(HomeAssistantError): class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" + + +class InvalidCodeFormat(HomeAssistantError): + """Error to indicate code format is wrong.""" + + +class InvalidCode(HomeAssistantError): + """Error to indicate the code is in wrong format""" diff --git a/custom_components/hikvision_axpro/const.py b/custom_components/hikvision_axpro/const.py index 1f54eee..b2fd7d9 100644 --- a/custom_components/hikvision_axpro/const.py +++ b/custom_components/hikvision_axpro/const.py @@ -1,5 +1,10 @@ """Constants for the hikvision_axpro integration.""" -DOMAIN = "hikvision_axpro" +from typing import Final -DATA_COORDINATOR = "hikaxpro" + +DOMAIN: Final = "hikvision_axpro" + +DATA_COORDINATOR: Final = "hikaxpro" + +USE_CODE_ARMING: Final = "use_code_arming" diff --git a/custom_components/hikvision_axpro/manifest.json b/custom_components/hikvision_axpro/manifest.json index 4400e99..c3e250f 100644 --- a/custom_components/hikvision_axpro/manifest.json +++ b/custom_components/hikvision_axpro/manifest.json @@ -10,5 +10,5 @@ "@gunkutzeybek" ], "iot_class": "local_polling", - "version": "0.0.1" + "version": "0.3.0" } \ No newline at end of file diff --git a/custom_components/hikvision_axpro/strings.json b/custom_components/hikvision_axpro/strings.json new file mode 100644 index 0000000..2de5be9 --- /dev/null +++ b/custom_components/hikvision_axpro/strings.json @@ -0,0 +1,28 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "enabled": "[%key:common::config_flow::data::enabled%]", + "code_format": "[%key:common::config_flow::data::code_format%]", + "use_code_arming": "[%key:common::config_flow::data::use_code_arming%]", + "code": "[%key:common::config_flow::data::code%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "invalid_code": "[%key:common::config_flow::error::invalid_code%]", + "invalid_code_format": "[%key:common::config_flow::error::invalid_code_format%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } + } + \ No newline at end of file diff --git a/custom_components/hikvision_axpro/translations/en.json b/custom_components/hikvision_axpro/translations/en.json index cf12154..068bf80 100644 --- a/custom_components/hikvision_axpro/translations/en.json +++ b/custom_components/hikvision_axpro/translations/en.json @@ -6,6 +6,8 @@ "error": { "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", + "invalid_code": "Inavlid code", + "invalid_code_format": "Invalid code format. Code format can take only NUMBER or TEXT as value.", "unknown": "Unexpected error" }, "step": { @@ -13,7 +15,11 @@ "data": { "host": "Host", "password": "Password", - "username": "Username" + "username": "Username", + "enabled": "Use code to disarm", + "code_format": "Code format (TEXT or NUMBER)", + "use_code_arming": "Use code for arming", + "code": "Code" } } }