├── .github └── workflows │ └── validate.yaml ├── LICENSE ├── README.md ├── custom_components └── hikvision_axpro │ ├── __init__.py │ ├── alarm_control_panel.py │ ├── config_flow.py │ ├── const.py │ ├── manifest.json │ ├── strings.json │ └── translations │ └── en.json └── hacs.json /.github/workflows/validate.yaml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 0 * * *" 8 | 9 | jobs: 10 | validate: 11 | runs-on: "ubuntu-latest" 12 | steps: 13 | - uses: "actions/checkout@v2" 14 | - name: HACS validation 15 | uses: "hacs/action@main" 16 | with: 17 | category: "integration" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 gunkutzeybek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | 3 | This repo is no longer maintained. Please check [peatrleocompel/hikaxpro_hacs](https://github.com/petrleocompel/hikaxpro_hacs) 4 | 5 | # hikaxpro_hacs 6 | HACS repository of Hikvision Ax Pro integration for home assistant 7 | 8 | [![hacs_badge](https://img.shields.io/badge/HACS-Custom-41BDF5.svg)](https://github.com/hacs/integration) 9 | -------------------------------------------------------------------------------- /custom_components/hikvision_axpro/__init__.py: -------------------------------------------------------------------------------- 1 | """The hikvision_axpro integration.""" 2 | import asyncio 3 | import logging 4 | import hikaxpro 5 | 6 | from async_timeout import timeout 7 | 8 | from homeassistant.components.alarm_control_panel import SCAN_INTERVAL 9 | from homeassistant.config_entries import ConfigEntry 10 | from homeassistant.const import ( 11 | ATTR_CODE_FORMAT, 12 | CONF_ENABLED, 13 | CONF_HOST, 14 | CONF_USERNAME, 15 | CONF_PASSWORD, 16 | CONF_CODE, 17 | Platform, 18 | ) 19 | from homeassistant.core import HomeAssistant 20 | from homeassistant.exceptions import ConfigEntryNotReady 21 | from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed 22 | 23 | from .const import DATA_COORDINATOR, DOMAIN, USE_CODE_ARMING 24 | 25 | PLATFORMS: list[Platform] = [Platform.ALARM_CONTROL_PANEL] 26 | _LOGGER = logging.getLogger(__name__) 27 | 28 | 29 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 30 | """Set up hikvision_axpro from a config entry.""" 31 | host = entry.data[CONF_HOST] 32 | username = entry.data[CONF_USERNAME] 33 | password = entry.data[CONF_PASSWORD] 34 | use_code = entry.data[CONF_ENABLED] 35 | code_format = entry.data[ATTR_CODE_FORMAT] 36 | code = entry.data[CONF_CODE] 37 | use_code_arming = entry.data[USE_CODE_ARMING] 38 | axpro = hikaxpro.HikAxPro(host, username, password) 39 | 40 | try: 41 | async with timeout(10): 42 | mac = await hass.async_add_executor_job(axpro.get_interface_mac_address, 1) 43 | except (asyncio.TimeoutError, ConnectionError) as ex: 44 | raise ConfigEntryNotReady from ex 45 | 46 | coordinator = HikAxProDataUpdateCoordinator( 47 | hass, 48 | axpro, 49 | mac, 50 | use_code, 51 | code_format, 52 | use_code_arming, 53 | code, 54 | ) 55 | hass.data.setdefault(DOMAIN, {}) 56 | hass.data[DOMAIN][entry.entry_id] = {DATA_COORDINATOR: coordinator} 57 | 58 | hass.config_entries.async_setup_platforms(entry, PLATFORMS) 59 | 60 | return True 61 | 62 | 63 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 64 | """Unload a config entry.""" 65 | if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): 66 | hass.data[DOMAIN].pop(entry.entry_id) 67 | 68 | return unload_ok 69 | 70 | 71 | class HikAxProDataUpdateCoordinator(DataUpdateCoordinator): 72 | """Class to manage fetching ax pro data.""" 73 | 74 | def __init__( 75 | self, 76 | hass, 77 | axpro, 78 | mac, 79 | use_code, 80 | code_format, 81 | use_code_arming, 82 | code, 83 | ): 84 | self.axpro = axpro 85 | self.state = None 86 | self.host = axpro.host 87 | self.mac = mac 88 | self.use_code = use_code 89 | self.code_format = code_format 90 | self.use_code_arming = use_code_arming 91 | self.code = code 92 | 93 | super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) 94 | 95 | def _update_data(self) -> None: 96 | """Fetch data from axpro via sync functions.""" 97 | status = self.axpro.get_area_arm_status(1) 98 | _LOGGER.debug("Axpro status: %s", status) 99 | 100 | self.state = status 101 | 102 | async def _async_update_data(self) -> None: 103 | """Fetch data from Axpro.""" 104 | try: 105 | async with timeout(10): 106 | await self.hass.async_add_executor_job(self._update_data) 107 | except ConnectionError as error: 108 | raise UpdateFailed(error) from error 109 | 110 | async def async_arm_home(self): 111 | """Arm alarm panel in home state.""" 112 | is_success = await self.hass.async_add_executor_job(self.axpro.arm_home) 113 | 114 | if is_success: 115 | await self._async_update_data() 116 | await self.async_request_refresh() 117 | 118 | async def async_arm_away(self): 119 | """Arm alarm panel in away state""" 120 | is_success = await self.hass.async_add_executor_job(self.axpro.arm_away) 121 | 122 | if is_success: 123 | await self._async_update_data() 124 | await self.async_request_refresh() 125 | 126 | async def async_disarm(self): 127 | """Disarm alarm control panel.""" 128 | is_success = await self.hass.async_add_executor_job(self.axpro.disarm) 129 | 130 | if is_success: 131 | await self._async_update_data() 132 | await self.async_request_refresh() 133 | -------------------------------------------------------------------------------- /custom_components/hikvision_axpro/alarm_control_panel.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from homeassistant.config_entries import ConfigEntry 4 | from homeassistant.core import HomeAssistant, callback 5 | from homeassistant.exceptions import HomeAssistantError 6 | from homeassistant.helpers.entity import DeviceInfo 7 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 8 | from homeassistant.helpers.update_coordinator import CoordinatorEntity 9 | from homeassistant.components.alarm_control_panel import ( 10 | AlarmControlPanelEntity, 11 | AlarmControlPanelEntityFeature, 12 | CodeFormat, 13 | ) 14 | 15 | from .const import DATA_COORDINATOR, DOMAIN 16 | 17 | 18 | async def async_setup_entry( 19 | hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback 20 | ) -> None: 21 | """Set up a Hikvision ax pro alarm control panel based on a config entry.""" 22 | coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] 23 | async_add_entities([HikAxProPanel(coordinator)], False) 24 | 25 | 26 | class HikAxProPanel(CoordinatorEntity, AlarmControlPanelEntity): 27 | """Representation of Hikvision Ax Pro alarm panel.""" 28 | 29 | @callback 30 | def _handle_coordinator_update(self) -> None: 31 | """Handle updated data from the coordinator.""" 32 | self.async_write_ha_state() 33 | 34 | _attr_supported_features = ( 35 | AlarmControlPanelEntityFeature.ARM_HOME 36 | | AlarmControlPanelEntityFeature.ARM_AWAY 37 | ) 38 | 39 | @property 40 | def device_info(self) -> DeviceInfo: 41 | """Return device info for this device.""" 42 | return DeviceInfo( 43 | identifiers={(DOMAIN, self.unique_id)}, 44 | manufacturer="Hikvision - Ax Pro", 45 | name=self.name, 46 | ) 47 | 48 | @property 49 | def unique_id(self): 50 | """Return a unique id.""" 51 | return self.coordinator.mac 52 | 53 | @property 54 | def name(self): 55 | """Return the name.""" 56 | return "HikvisionAxPro" 57 | 58 | @property 59 | def state(self): 60 | """Return the state of the device.""" 61 | return self.coordinator.state 62 | 63 | @property 64 | def code_format(self) -> CodeFormat | None: 65 | """Return the code format.""" 66 | return self.__get_code_format(self.coordinator.code_format) 67 | 68 | def __get_code_format(self, code_format_str) -> CodeFormat: 69 | """Returns CodeFormat according to the given code fomrat string.""" 70 | code_format: CodeFormat = None 71 | 72 | if not self.coordinator.use_code: 73 | code_format = None 74 | elif code_format_str == "NUMBER": 75 | code_format = CodeFormat.NUMBER 76 | elif code_format_str == "TEXT": 77 | code_format = CodeFormat.TEXT 78 | 79 | return code_format 80 | 81 | async def async_alarm_disarm(self, code=None): 82 | """Send disarm command.""" 83 | if self.coordinator.use_code: 84 | if not self.__is_code_valid(code): 85 | return 86 | 87 | await self.coordinator.async_disarm() 88 | 89 | async def async_alarm_arm_home(self, code=None): 90 | """Send arm home command.""" 91 | if self.coordinator.use_code and self.coordinator.use_code_arming: 92 | if not self.__is_code_valid(code): 93 | return 94 | 95 | await self.coordinator.async_arm_home() 96 | 97 | async def async_alarm_arm_away(self, code=None): 98 | """Send arm away command.""" 99 | if self.coordinator.use_code and self.coordinator.use_code_arming: 100 | if not self.__is_code_valid(code): 101 | return 102 | 103 | await self.coordinator.async_arm_away() 104 | 105 | def __is_code_valid(self, code): 106 | return code == self.coordinator.code 107 | -------------------------------------------------------------------------------- /custom_components/hikvision_axpro/config_flow.py: -------------------------------------------------------------------------------- 1 | """Config flow for hikvision_axpro integration.""" 2 | import logging 3 | from typing import Any 4 | 5 | import voluptuous as vol 6 | 7 | import hikaxpro 8 | 9 | from homeassistant import config_entries 10 | from homeassistant.core import HomeAssistant 11 | from homeassistant.data_entry_flow import FlowResult 12 | from homeassistant.exceptions import HomeAssistantError 13 | from homeassistant.const import ( 14 | CONF_CODE, 15 | CONF_ENABLED, 16 | ATTR_CODE_FORMAT, 17 | CONF_HOST, 18 | CONF_USERNAME, 19 | CONF_PASSWORD, 20 | ) 21 | 22 | from .const import DOMAIN, USE_CODE_ARMING 23 | 24 | _LOGGER = logging.getLogger(__name__) 25 | 26 | STEP_USER_DATA_SCHEMA = vol.Schema( 27 | { 28 | vol.Required(CONF_HOST): str, 29 | vol.Required(CONF_USERNAME): str, 30 | vol.Required(CONF_PASSWORD): str, 31 | vol.Required(CONF_ENABLED, default=False): bool, 32 | vol.Optional(ATTR_CODE_FORMAT, default="NUMBER"): vol.In(["TEXT", "NUMBER"]), 33 | vol.Optional(CONF_CODE, default=""): str, 34 | vol.Optional(USE_CODE_ARMING, default=False): bool, 35 | } 36 | ) 37 | 38 | 39 | class AxProHub: 40 | """Helper class for validation and setup ops.""" 41 | 42 | def __init__( 43 | self, host: str, username: str, password: str, hass: HomeAssistant 44 | ) -> None: 45 | self.host = host 46 | self.username = username 47 | self.password = password 48 | self.axpro = hikaxpro.HikAxPro(host, username, password) 49 | self.hass = hass 50 | 51 | async def authenticate(self) -> bool: 52 | """Check the provided credentials by connecting to ax pro.""" 53 | is_connect_success = await self.hass.async_add_executor_job(self.axpro.connect) 54 | return is_connect_success 55 | 56 | 57 | async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: 58 | """Validate the user input allows us to connect. 59 | 60 | Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. 61 | """ 62 | 63 | if data[CONF_ENABLED]: 64 | if data[ATTR_CODE_FORMAT] is None or ( 65 | data[ATTR_CODE_FORMAT] != "NUMBER" and data[ATTR_CODE_FORMAT] != "TEXT" 66 | ): 67 | raise InvalidCodeFormat 68 | 69 | if ( 70 | data[CONF_CODE] is None 71 | or data[CONF_CODE] == "" 72 | or (data[ATTR_CODE_FORMAT] == "NUMBER" and not str.isdigit(data[CONF_CODE])) 73 | ): 74 | raise InvalidCode 75 | 76 | hub = AxProHub(data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD], hass) 77 | 78 | if not await hub.authenticate(): 79 | raise InvalidAuth 80 | 81 | return {"title": f"Hikvision_axpro_{data['host']}"} 82 | 83 | 84 | class AxProConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 85 | """Handle a config flow for hikvision_axpro.""" 86 | 87 | VERSION = 1 88 | 89 | async def async_step_user(self, user_input=None) -> FlowResult: 90 | """Handle the initial step.""" 91 | if user_input is None: 92 | return self.async_show_form( 93 | step_id="user", data_schema=STEP_USER_DATA_SCHEMA 94 | ) 95 | 96 | errors = {} 97 | 98 | try: 99 | info = await validate_input(self.hass, user_input) 100 | except CannotConnect: 101 | errors["base"] = "cannot_connect" 102 | except InvalidAuth: 103 | errors["base"] = "invalid_auth" 104 | except InvalidCodeFormat: 105 | errors["base"] = "invalid_code_format" 106 | except InvalidCode: 107 | errors["base"] = "invalid_code" 108 | except Exception: # pylint: disable=broad-except 109 | _LOGGER.exception("Unexpected exception") 110 | errors["base"] = "unknown" 111 | else: 112 | return self.async_create_entry(title=info["title"], data=user_input) 113 | 114 | return self.async_show_form( 115 | step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors 116 | ) 117 | 118 | 119 | class CannotConnect(HomeAssistantError): 120 | """Error to indicate we cannot connect.""" 121 | 122 | 123 | class InvalidAuth(HomeAssistantError): 124 | """Error to indicate there is invalid auth.""" 125 | 126 | 127 | class InvalidCodeFormat(HomeAssistantError): 128 | """Error to indicate code format is wrong.""" 129 | 130 | 131 | class InvalidCode(HomeAssistantError): 132 | """Error to indicate the code is in wrong format""" 133 | -------------------------------------------------------------------------------- /custom_components/hikvision_axpro/const.py: -------------------------------------------------------------------------------- 1 | """Constants for the hikvision_axpro integration.""" 2 | 3 | from typing import Final 4 | 5 | 6 | DOMAIN: Final = "hikvision_axpro" 7 | 8 | DATA_COORDINATOR: Final = "hikaxpro" 9 | 10 | USE_CODE_ARMING: Final = "use_code_arming" 11 | -------------------------------------------------------------------------------- /custom_components/hikvision_axpro/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "hikvision_axpro", 3 | "name": "hikvision_axpro", 4 | "config_flow": true, 5 | "documentation": "https://www.home-assistant.io/integrations/hikvision_axpro", 6 | "issue_tracker": "https://github.com/gunkutzeybek/hikaxpro_hacs/issues", 7 | "requirements": ["hikaxpro==1.3.0"], 8 | "dependencies": [], 9 | "codeowners": [ 10 | "@gunkutzeybek" 11 | ], 12 | "iot_class": "local_polling", 13 | "version": "0.3.0" 14 | } -------------------------------------------------------------------------------- /custom_components/hikvision_axpro/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "data": { 6 | "host": "[%key:common::config_flow::data::host%]", 7 | "username": "[%key:common::config_flow::data::username%]", 8 | "password": "[%key:common::config_flow::data::password%]", 9 | "enabled": "[%key:common::config_flow::data::enabled%]", 10 | "code_format": "[%key:common::config_flow::data::code_format%]", 11 | "use_code_arming": "[%key:common::config_flow::data::use_code_arming%]", 12 | "code": "[%key:common::config_flow::data::code%]" 13 | } 14 | } 15 | }, 16 | "error": { 17 | "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", 18 | "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", 19 | "invalid_code": "[%key:common::config_flow::error::invalid_code%]", 20 | "invalid_code_format": "[%key:common::config_flow::error::invalid_code_format%]", 21 | "unknown": "[%key:common::config_flow::error::unknown%]" 22 | }, 23 | "abort": { 24 | "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /custom_components/hikvision_axpro/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Device is already configured" 5 | }, 6 | "error": { 7 | "cannot_connect": "Failed to connect", 8 | "invalid_auth": "Invalid authentication", 9 | "invalid_code": "Inavlid code", 10 | "invalid_code_format": "Invalid code format. Code format can take only NUMBER or TEXT as value.", 11 | "unknown": "Unexpected error" 12 | }, 13 | "step": { 14 | "user": { 15 | "data": { 16 | "host": "Host", 17 | "password": "Password", 18 | "username": "Username", 19 | "enabled": "Use code to disarm", 20 | "code_format": "Code format (TEXT or NUMBER)", 21 | "use_code_arming": "Use code for arming", 22 | "code": "Code" 23 | } 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Hikvision Ax Pro integration", 3 | "render_readme": true 4 | } --------------------------------------------------------------------------------