├── hacs.json ├── custom_components └── sauresha │ ├── hacs.json │ ├── manifest.json │ ├── translations │ ├── en.json │ └── ru.json │ ├── switch.py │ ├── const.py │ ├── binary_sensor.py │ ├── sensor.py │ ├── classes.py │ ├── __init__.py │ ├── README.md │ ├── info.md │ ├── config_flow.py │ ├── api.py │ └── entity.py ├── LICENSE ├── README.md ├── info.md └── .gitignore /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Integration Saures controllers with HA", 3 | "domains": ["binary_sensor", "sensor", "switch"], 4 | "homeassistant": "0.104", 5 | "country": ["RU"] 6 | } 7 | -------------------------------------------------------------------------------- /custom_components/sauresha/hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Integration Saures controllers with HA", 3 | "domains": ["binary_sensor", "sensor", "switch"], 4 | "homeassistant": "0.104", 5 | "country": ["RU"] 6 | } 7 | -------------------------------------------------------------------------------- /custom_components/sauresha/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "sauresha", 3 | "name": "SauresHA", 4 | "documentation": "https://github.com/volshebniks/sauresha", 5 | "issue_tracker": "https://github.com/volshebniks/sauresha/issues", 6 | "requirements": [], 7 | "dependencies": [], 8 | "version": "1.0.4", 9 | "config_flow": true, 10 | "codeowners": [ 11 | "@volshebniks" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /custom_components/sauresha/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "no_mixed_config": "Settings is complited already. Настройка через configuration.yaml несовместима с настройкой через интерфейс." 5 | }, 6 | "error": { 7 | "cannot_connect": "Не удалось подключиться." 8 | }, 9 | "step": { 10 | "user": { 11 | "data": { 12 | "email": "login (email)", 13 | "password": "password", 14 | "scan_interval": "pause between updates (min)" 15 | }, 16 | "description": "Read the documentation, if you need a help :\nhttps://github.com/volshebniks/sauresha", 17 | "title": "Integration HA with Saures" 18 | } 19 | } 20 | }, 21 | "options": { 22 | "step": { 23 | "init": { 24 | "data": { 25 | "flat": "flats" 26 | }, 27 | "title": "Saures settings" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /custom_components/sauresha/translations/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "no_mixed_config": "Настройка уже выполнена. Настройка через configuration.yaml несовместима с настройкой через интерфейс." 5 | }, 6 | "error": { 7 | "cannot_connect": "Не удалось подключиться." 8 | }, 9 | "step": { 10 | "user": { 11 | "data": { 12 | "email": "имя пользователя (email)", 13 | "password": "пароль", 14 | "scan_interval": "пауза между обновлениями (мин)" 15 | }, 16 | "description": "Ознакомьтесь с инструкциями, если Вам нужна помощь с настройкой:\nhttps://github.com/volshebniks/sauresha", 17 | "title": "Интеграция HA c Saures" 18 | } 19 | } 20 | }, 21 | "options": { 22 | "step": { 23 | "init": { 24 | "data": { 25 | "flat": "квартиры" 26 | }, 27 | "title": "Настройки Saures" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Pinkywafer 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 | -------------------------------------------------------------------------------- /custom_components/sauresha/switch.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from homeassistant.const import CONF_SCAN_INTERVAL 3 | from datetime import timedelta 4 | from homeassistant.core import HomeAssistant 5 | 6 | from .const import DOMAIN, COORDINATOR, CONF_ISDEBUG 7 | from .api import SauresHA 8 | from .entity import SauresSwitch 9 | 10 | SCAN_INTERVAL = timedelta(minutes=20) 11 | 12 | _LOGGER = logging.getLogger(__name__) 13 | 14 | 15 | async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities): 16 | """Setup switch platform.""" 17 | my_sensors: list = [] 18 | is_debug = CONF_ISDEBUG 19 | scan_interval = config_entry.data.get(CONF_SCAN_INTERVAL) 20 | 21 | controller: SauresHA = hass.data[DOMAIN].get(COORDINATOR) 22 | for curflat in controller.flats: 23 | try: 24 | sensors = await controller.async_get_switches(curflat, False) 25 | for curSensor in sensors: 26 | sensor = SauresSwitch( 27 | hass, 28 | controller, 29 | curflat, 30 | curSensor.get("meter_id"), 31 | curSensor.get("sn"), 32 | curSensor.get("meter_name"), 33 | is_debug, 34 | scan_interval, 35 | ) 36 | my_sensors.append(sensor) 37 | except Exception: 38 | _LOGGER.exception(str(Exception)) 39 | 40 | if my_sensors: 41 | async_add_entities(my_sensors, True) 42 | -------------------------------------------------------------------------------- /custom_components/sauresha/const.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020-2022, Sergey Golynskiy 2 | # Creative Commons BY-NC-SA 4.0 International Public License 3 | # (see LICENSE.md or https://creativecommons.org/licenses/by-nc-sa/4.0/) 4 | """ 5 | The Saures component. 6 | 7 | For more details about this platform, please refer to the documentation at 8 | https://github.com/volshebniks/sauresha/ 9 | """ 10 | 11 | # Base component constants 12 | from xmlrpc.client import Boolean 13 | 14 | 15 | NAME = "Saures" 16 | DOMAIN = "sauresha" 17 | VERSION = "1.0.0" 18 | ATTRIBUTION = "Home assistant component for Saures" 19 | ISSUE_URL = "https://github.com/volshebniks/sauresha/issues" 20 | 21 | PLATFORMS = ["binary_sensor", "sensor", "switch"] 22 | 23 | DOMAIN = "sauresha" 24 | 25 | STARTUP_MESSAGE = f""" 26 | ------------------------------------------------------------------- 27 | {NAME} 28 | Version: {VERSION} 29 | This is a custom integration! 30 | If you have ANY issues with this you need to open an issue here: 31 | {ISSUE_URL} 32 | ------------------------------------------------------------------- 33 | """ 34 | 35 | # Configuration and options 36 | CONF_ISDEBUG: Boolean = False 37 | CONF_DEBUG = "debug" 38 | CONF_FLATS = "flats" 39 | CONF_FLAT_ID = "flat_id" 40 | CONF_SENSORS = "sensors" 41 | CONF_BINARY_SENSORS_DEF = [9, 10] # On Saures API [9 = Датчик] [10 = Состояние крана] 42 | CONF_BINARY_SENSOR_DEV_CLASS_MOISTURE_DEF = [9] 43 | CONF_BINARY_SENSOR_DEV_CLASS_OPENING_DEF = [10] 44 | CONF_SWITCH_DEF = [6] 45 | 46 | COORDINATOR = "coordinator" 47 | 48 | # Command 49 | CONF_COMMAND_ACTIVATE = "activate" 50 | CONF_COMMAND_DEACTIVATE = "deactivate" 51 | -------------------------------------------------------------------------------- /custom_components/sauresha/binary_sensor.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from homeassistant.const import CONF_SCAN_INTERVAL 3 | from datetime import timedelta 4 | from homeassistant.core import HomeAssistant 5 | 6 | from .const import DOMAIN, COORDINATOR, CONF_ISDEBUG 7 | from .api import SauresHA 8 | from .entity import SauresBinarySensor 9 | 10 | 11 | _LOGGER = logging.getLogger(__name__) 12 | 13 | SCAN_INTERVAL = timedelta(minutes=20) 14 | 15 | 16 | async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): 17 | """Setup the sensor platform.""" 18 | _LOGGER.exception( 19 | "The sauresha platform for the binary sensor integration does not support YAML platform setup. Please remove it from your config" 20 | ) 21 | return True 22 | 23 | 24 | async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities): 25 | """Setup sensor platform.""" 26 | my_sensors: list = [] 27 | is_debug = CONF_ISDEBUG 28 | scan_interval = config_entry.data.get(CONF_SCAN_INTERVAL) 29 | 30 | controller: SauresHA = hass.data[DOMAIN].get(COORDINATOR) 31 | for curflat in controller.flats: 32 | try: 33 | sensors = await controller.async_get_binary_sensors(curflat) 34 | for curSensor in sensors: 35 | sensor = SauresBinarySensor( 36 | hass, 37 | controller, 38 | curflat, 39 | curSensor.get("type", {}).get("number"), 40 | curSensor.get("meter_id"), 41 | curSensor.get("sn"), 42 | curSensor.get("meter_name"), 43 | is_debug, 44 | scan_interval, 45 | ) 46 | my_sensors.append(sensor) 47 | except Exception: 48 | _LOGGER.exception(str(Exception)) 49 | 50 | if my_sensors: 51 | async_add_entities(my_sensors, True) 52 | -------------------------------------------------------------------------------- /custom_components/sauresha/sensor.py: -------------------------------------------------------------------------------- 1 | """Provides a sensor for Saures.""" 2 | import logging 3 | from homeassistant.const import CONF_SCAN_INTERVAL 4 | from datetime import timedelta 5 | 6 | from homeassistant.core import HomeAssistant 7 | from .const import DOMAIN, COORDINATOR, CONF_ISDEBUG 8 | from .api import SauresHA 9 | from .entity import SauresControllerSensor, SauresSensor 10 | 11 | SCAN_INTERVAL = timedelta(minutes=20) 12 | 13 | _LOGGER = logging.getLogger(__name__) 14 | 15 | 16 | async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): 17 | """Setup the sensor platform.""" 18 | _LOGGER.exception( 19 | "The sauresha platform for the sensor integration does not support YAML platform setup. Please remove it from your config" 20 | ) 21 | return True 22 | 23 | 24 | async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities): 25 | """Setup sensor platform.""" 26 | my_sensors: list = [] 27 | is_debug = CONF_ISDEBUG 28 | scan_interval = config_entry.data.get(CONF_SCAN_INTERVAL) 29 | controller: SauresHA = hass.data[DOMAIN].get(COORDINATOR) 30 | for curflat in controller.flats: 31 | try: 32 | controllers = await controller.async_get_controllers(curflat) 33 | for obj in controllers: 34 | if len(obj.get("sn")) > 0: 35 | my_controller = SauresControllerSensor( 36 | hass, 37 | controller, 38 | curflat, 39 | obj.get("sn"), 40 | obj.get("name"), 41 | is_debug, 42 | scan_interval, 43 | ) 44 | my_sensors.append(my_controller) 45 | 46 | sensors = await controller.async_get_sensors(curflat) 47 | for curSensor in sensors: 48 | sensor = SauresSensor( 49 | hass, 50 | controller, 51 | curflat, 52 | curSensor.get("meter_id"), 53 | curSensor.get("sn"), 54 | curSensor.get("meter_name"), 55 | is_debug, 56 | scan_interval, 57 | ) 58 | my_sensors.append(sensor) 59 | except Exception: 60 | _LOGGER.exception(str(Exception)) 61 | 62 | if my_sensors: 63 | async_add_entities(my_sensors, True) 64 | -------------------------------------------------------------------------------- /custom_components/sauresha/classes.py: -------------------------------------------------------------------------------- 1 | """Saures entity base class.""" 2 | 3 | 4 | class SauresController: 5 | def __init__(self, data): 6 | self.data = data 7 | self.name = data.get("sn") 8 | self.sn = data.get("sn") 9 | self.battery = data.get("bat") 10 | self.ssid = data.get("ssid") 11 | self.local_ip = data.get("local_ip") 12 | self.firmware = data.get("firmware") 13 | self.readout_dt = data.get("readout_dt") 14 | self.request_dt = data.get("request_dt") 15 | self.last_connection = data.get("last_connection") 16 | self.state = "OK" 17 | self.rssi = data.get("rssi") 18 | self.hardware = data.get("hardware") 19 | self.new_firmware = data.get("new_firmware") 20 | self.last_connection = data.get("last_connection") 21 | self.last_connection_warning = data.get("last_connection_warning") 22 | self.check_hours = data.get("check_hours") 23 | self.check_period_display = data.get("check_period_display") 24 | self.requests = data.get("requests") 25 | self.log = data.get("log") 26 | self.cap_state = bool(data.get("cap_state")) 27 | self.power_supply = bool(data.get("power_supply")) 28 | 29 | 30 | class SauresSensor: 31 | def __init__(self, data): 32 | self.data = data 33 | self.name = data.get("meter_name") 34 | self.type_number = data.get("type", {}).get("number") 35 | self.type = data.get("type", {}).get("name") 36 | self.state = data.get("state", {}).get("name") 37 | self.sn = data.get("sn") 38 | self.value = data.get("value") 39 | self.meter_id = data.get("meter_id") 40 | self.input = data.get("input") 41 | self.approve_dt = data.get("approve_dt") 42 | 43 | self.values = data.get("vals", []) 44 | 45 | if len(self.values) == 2: 46 | self.value = "{0}/{1}".format(self.values[0], self.values[1]) 47 | self.t1 = self.values[0] 48 | self.t2 = self.values[1] 49 | self.t3 = "-" 50 | self.t4 = "-" 51 | elif len(self.values) == 3: 52 | self.value = "{0}/{1}/{2}".format( 53 | self.values[0], self.values[1], self.values[2] 54 | ) 55 | self.t1 = self.values[0] 56 | self.t2 = self.values[1] 57 | self.t3 = self.values[2] 58 | self.t4 = "-" 59 | elif len(self.values) == 4: 60 | self.value = "{0}/{1}/{2}/{3}".format( 61 | self.values[0], self.values[1], self.values[2], self.values[3] 62 | ) 63 | self.t1 = self.values[0] 64 | self.t2 = self.values[1] 65 | self.t3 = self.values[2] 66 | self.t4 = self.values[3] 67 | elif len(self.values) == 1: 68 | self.value = self.values[0] 69 | self.t1 = self.values[0] 70 | self.t2 = "-" 71 | self.t3 = "-" 72 | self.t4 = "-" 73 | -------------------------------------------------------------------------------- /custom_components/sauresha/__init__.py: -------------------------------------------------------------------------------- 1 | """Support for Saures Connect appliances.""" 2 | 3 | import logging 4 | from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry 5 | from homeassistant import config_entries 6 | 7 | from homeassistant.core import HomeAssistant 8 | from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_SCAN_INTERVAL 9 | 10 | from .api import SauresHA 11 | from .const import ( 12 | DOMAIN, 13 | CONF_DEBUG, 14 | CONF_FLATS, 15 | CONF_FLAT_ID, 16 | CONF_ISDEBUG, 17 | STARTUP_MESSAGE, 18 | PLATFORMS, 19 | COORDINATOR, 20 | ) 21 | 22 | _LOGGER = logging.getLogger(__name__) 23 | 24 | 25 | async def async_setup(hass: HomeAssistant, config: dict) -> bool: 26 | """Set up component.""" 27 | 28 | domain_config = config.get("sensor") 29 | if not domain_config: 30 | return True 31 | 32 | yaml_config = {} 33 | hass.data[DOMAIN] = yaml_config 34 | 35 | for user_cfg in domain_config: 36 | if not user_cfg: 37 | continue 38 | if not user_cfg.get(CONF_EMAIL): 39 | continue 40 | if not user_cfg.get(CONF_PASSWORD): 41 | continue 42 | 43 | yaml_email: str = user_cfg[CONF_EMAIL] 44 | yaml_password: str = user_cfg[CONF_PASSWORD] 45 | yaml_flatid = user_cfg[CONF_FLAT_ID] 46 | 47 | user_input = { 48 | CONF_EMAIL: yaml_email, 49 | CONF_PASSWORD: yaml_password, 50 | CONF_SCAN_INTERVAL: 30, 51 | CONF_FLATS: yaml_flatid, 52 | } 53 | hass.async_create_task( 54 | hass.config_entries.flow.async_init( 55 | DOMAIN, 56 | context={ 57 | "source": config_entries.SOURCE_IMPORT, 58 | "title": user_input[CONF_EMAIL], 59 | }, 60 | data=user_input, 61 | ) 62 | ) 63 | 64 | # Print startup messages 65 | hass.data.setdefault(DOMAIN, {}) 66 | _LOGGER.info(STARTUP_MESSAGE) 67 | # Clean up old imports from configuration.yaml 68 | for entry in hass.config_entries.async_entries(DOMAIN): 69 | if entry.source == SOURCE_IMPORT: 70 | await hass.config_entries.async_remove(entry.entry_id) 71 | 72 | return True 73 | 74 | 75 | async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: 76 | cur_config = config_entry.data 77 | cur_options = config_entry.options 78 | curFlats = {} 79 | if cur_options.get(CONF_FLATS): 80 | curFlats = cur_options.get(CONF_FLATS) 81 | 82 | SauresAPI: SauresHA = SauresHA( 83 | hass, 84 | cur_config.get(CONF_EMAIL), 85 | cur_config.get(CONF_PASSWORD), 86 | CONF_ISDEBUG, 87 | curFlats, 88 | ) 89 | await SauresAPI.async_fetch_data() 90 | 91 | hass.data[DOMAIN] = { 92 | CONF_SCAN_INTERVAL: cur_config.get(CONF_SCAN_INTERVAL), 93 | CONF_DEBUG: CONF_ISDEBUG, 94 | COORDINATOR: SauresAPI, 95 | } 96 | await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) 97 | return True 98 | 99 | 100 | async def async_migrate_entry(hass, config_entry: ConfigEntry): 101 | return True 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SauresHA 2 | [![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg)](https://github.com/custom-components/hacs) 3 | [![GitHub release (latest by date)](https://img.shields.io/github/v/release/volshebniks/sauresha)](https://github.com/volshebniks/sauresha/releases) 4 | ![GitHub Release Date](https://img.shields.io/github/release-date/volshebniks/sauresha) 5 | [![GitHub](https://img.shields.io/github/license/volshebniks/sauresha)](LICENSE) 6 | 7 | [![Maintenance](https://img.shields.io/badge/Maintained%3F-Yes-brightgreen.svg)](https://github.com/volshebniks/sauresha/graphs/commit-activity) 8 | [![GitHub issues](https://img.shields.io/github/issues/volshebniks/sauresha)](https://github.com/volshebniks/sauresha/issues) 9 | 10 | [![Donate](https://img.shields.io/badge/donate-Coffee-yellow.svg)](https://https://www.buymeacoffee.com/RlnBV9r) 11 | [![Donate](https://img.shields.io/badge/donate-Yandex-red.svg)](https://yoomoney.ru/to/41001566881198/150) 12 | 13 | Пожертвование на развитие проекта [Яндекс.Деньги](https://yoomoney.ru/to/41001566881198) 14 | 15 | ## Update 1: Начиная с версии 0.3: 16 | * сделан переход на новое клиентское API 17 | * добавлена необязательная настройка для sensor - scan_interval. Время обновления в минутах. По умолчанию = 10 минут. 18 | * из-за перехода появились новые атрибуты у сенсоров. 19 | 20 | ## Update 2: версия 0.3.5: 21 | * уменьшено количество вызово API 22 | * ускорена первоначальная инициализация модуля 23 | * сделан переход на асинхронные методы 24 | * исправлена ошибка с заднием своего scan_interval 25 | * исправлена ошибка связанная с наличием русских букв в серийных номерах 26 | * на стороне Saures явно починили кеширование для нового API 27 | 28 | ## Update 3: версия 0.3.8: 29 | * вернул синхронные вызовы 30 | 31 | ## Update 4: Начиная с версии 0.5: 32 | * Существенно сокращено кол-во обращений к серверу Saures, для предотвращения блокировки. 33 |
Рекомендую в настройках указать: 34 | ```yaml 35 | scan_interval: 36 | minutes: 30 37 | ``` 38 | Иначе могут быть блокировки в будущем. 39 | 40 | ## Update 5: Начиная с версии 0.6: 41 | * значительно изменен механизм настройки 42 | * можно задавать свои мена для всего 43 | * можно в настройках делать ссылки на !secret 44 | * добавил в manifest, version 45 | 46 | ## Update 6: Начиная с версии 1.0: 47 | * полностью переработан код для минимизации обращений к серверу Saures 48 | * внедрен асинхронный режим работы 49 | * Добавлено управление кранами 50 | * настройка через GUI 51 | 52 | ## Содержание 53 | 54 | * [Установка](#устнановка) 55 | * [Ручная установка](#ручная-установка) 56 | * [Установка через HACS](#hacs_установка) 57 | 58 | Для связи: 59 | 60 | Интеграция котроллеров [Saures](https://www.saures.ru) c [Home Assistant](https://www.home-assistant.io/) 61 | # Описание 62 | 63 | В настоящее время поддерживаются следующие типы устройств от Saurus 64 | 1. Счетчик холодной воды (м³) = sensor в Home Assistant 65 | 2. Счетчик горячей воды (м³) = sensor в Home Assistant 66 | 3. Счетчик газа (м³) = sensor в Home Assistant 67 | 4. Датчик протечки (0 – нет протечки, 1 - протечка) = binary_sensor в Home Assistant 68 | 5. Датчик температуры (градусы) = sensor в Home Assistant 69 | 6. Электро-шаровой кран управление (0 – открыться, 1 - закрыться) - поддерживается, switch в Home Assistant 70 | 7. Счетчик тепла (кВт*ч) = sensor в Home Assistant 71 | 8. Счетчик электричества (кВт*ч) (в том числе многотарифные) = sensor в Home Assistant 72 | 9. Сухой контакт (0 – деактивирован, 1 – активирован) = binary_sensor в Home Assistant 73 | 10. Электро-шаровой кран состояние (0 – не подключен модуль, 1 – неизвестное состояние, 2 – открыт, 3 - закрыт) = sensor в Home Assistant 74 | 11. Непосредственно сами контроллеры = sensor в Home Assistant 75 | 76 | ## Установка 77 | 78 | ### Ручная установка 79 | 80 | 1. Добавляем компонент в Home Assistant 81 | Распаковываем архив. Папку sauresha берем целиком и копируем в custom_components. 82 | 2. Осуществляем конфигурацию компонента в Home Assistant через GUI. 83 | 3. Перезагружаем HA 84 | 85 | ### HACS установка 86 | 87 | 1. Убедитесь, что [HACS](https://custom-components.github.io/hacs/) уже устновлен. 88 | 2. Перейдите на закладку SETTINGS 89 | 3. Введите https://github.com/volshebniks/sauresha и выберите категорию Integration, нажмите Сохранить 90 | 4. Новый репозиторий Integration Saures controllers with HA будет добавлен на закладке Integration 91 | 5. Устновите SauresHA из него 92 | 3. Осуществляем конфигурацию компонента в Home Assistant через GUI. 93 | 4. Перезапустите HA. 94 | 95 | # План развития проекта 96 | - [X] Добавить проект в HACS 97 | - [ ] Сделать сенсоры для счетчиков с показаниями за день/месяц/год 98 | - [X] Добавить управление кранами 99 | - [ ] Сделать pallete для Node-Red 100 | - [X] Сделать полноценную интеграцию с Home Assistant (добавляется в раздел интеграции) 101 | 102 | 103 | # Credits 104 | 105 | Большое спасибо следующим организациям и проектам, работа которых имеет важное значение для развития проекта: 106 | 107 | Нет их пока :) 108 | 109 | ---------------------------------------------------------------------------------------------------------------------------------- 110 | Пожертвование на развитие проекта [Яндекс.Деньги](https://money.yandex.ru/to/41001566881198) 111 | -------------------------------------------------------------------------------- /info.md: -------------------------------------------------------------------------------- 1 | # SauresHA 2 | [![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg)](https://github.com/custom-components/hacs) 3 | [![GitHub release (latest by date)](https://img.shields.io/github/v/release/volshebniks/sauresha)](https://github.com/volshebniks/sauresha/releases) 4 | ![GitHub Release Date](https://img.shields.io/github/release-date/volshebniks/sauresha) 5 | [![GitHub](https://img.shields.io/github/license/volshebniks/sauresha)](LICENSE) 6 | 7 | [![Maintenance](https://img.shields.io/badge/Maintained%3F-Yes-brightgreen.svg)](https://github.com/volshebniks/sauresha/graphs/commit-activity) 8 | [![GitHub issues](https://img.shields.io/github/issues/volshebniks/sauresha)](https://github.com/volshebniks/sauresha/issues) 9 | 10 | [![Donate](https://img.shields.io/badge/donate-Coffee-yellow.svg)](https://https://www.buymeacoffee.com/RlnBV9r) 11 | [![Donate](https://img.shields.io/badge/donate-Yandex-red.svg)](https://money.yandex.ru/to/41001566881198) 12 | 13 | Пожертвование на развитие проекта [Яндекс.Деньги](https://money.yandex.ru/to/41001566881198) 14 | 15 | ## Update 1: Начиная с версии 0.3: 16 | * сделан переход на новое клиентское API 17 | * добавлена необязательная настройка для sensor - scan_interval. Время обновления в минутах. По умолчанию = 10 минут. 18 | * из-за перехода появились новые атрибуты у сенсоров. 19 | 20 | ## Update 2: версия 0.3.5: 21 | * уменьшено количество вызово API 22 | * ускорена первоначальная инициализация модуля 23 | * сделан переход на асинхронные методы 24 | * исправлена ошибка с заднием своего scan_interval 25 | * исправлена ошибка связанная с наличием русских букв в серийных номерах 26 | * на стороне Saures явно починили кеширование для нового API 27 | 28 | ## Update 3: версия 0.3.8: 29 | * вернул синхронные вызовы 30 | 31 | ## Update 4: Начиная с версии 0.5: 32 | * Существенно сокращено кол-во обращений к серверу Saures, для предотвращения блокировки. 33 |
Рекомендую в настройках указать: 34 | ```yaml 35 | scan_interval: 36 | minutes: 30 37 | ``` 38 | Иначе могут быть блокировки в будущем. 39 | 40 | ## Update 5: Начиная с версии 0.6: 41 | * значительно изменен механизм настройки 42 | * можно задавать свои мена для всего 43 | * можно в настройках делать ссылки на !secret 44 | * добавил в manifest, version 45 | 46 | ## Update 6: Начиная с версии 1.0: 47 | * полностью переработан код для минимизации обращений к серверу Saures 48 | * внедрен асинхронный режим работы 49 | * Добавлено управление кранами 50 | * настройка через GUI 51 | 52 | ## Содержание 53 | 54 | * [Установка](#устнановка) 55 | * [Ручная установка](#ручная-установка) 56 | * [Установка через HACS](#hacs_установка) 57 | 58 | 59 | 60 | Для связи: 61 | 62 | Интеграция котроллеров [Saures](https://www.saures.ru) c [Home Assistant](https://www.home-assistant.io/) 63 | # Описание 64 | 65 | В настоящее время поддерживаются следующие типы устройств от Saurus 66 | 1. Счетчик холодной воды (м³) = sensor в Home Assistant 67 | 2. Счетчик горячей воды (м³) = sensor в Home Assistant 68 | 3. Счетчик газа (м³) = sensor в Home Assistant 69 | 4. Датчик протечки (0 – нет протечки, 1 - протечка) = binary_sensor в Home Assistant 70 | 5. Датчик температуры (градусы) = sensor в Home Assistant 71 | 6. Электро-шаровой кран управление (0 – открыться, 1 - закрыться) - поддерживается, switch в Home Assistant 72 | 7. Счетчик тепла (кВт*ч) = sensor в Home Assistant 73 | 8. Счетчик электричества (кВт*ч) (в том числе многотарифные) = sensor в Home Assistant 74 | 9. Сухой контакт (0 – деактивирован, 1 – активирован) = binary_sensor в Home Assistant 75 | 10. Электро-шаровой кран состояние (0 – не подключен модуль, 1 – неизвестное состояние, 2 – открыт, 3 - закрыт) = sensor в Home Assistant 76 | 11. Непосредственно сами контроллеры = sensor в Home Assistant 77 | 78 | ## Установка 79 | 80 | ### Ручная установка 81 | 82 | 1. Добавляем компонент в Home Assistant 83 | Распаковываем архив. Папку sauresha берем целиком и копируем в custom_components. 84 | 2. Осуществляем конфигурацию компонента в Home Assistant через GUI. 85 | 3. Перезагружаем HA 86 | 87 | ### HACS установка 88 | 89 | 1. Убедитесь, что [HACS](https://custom-components.github.io/hacs/) уже устновлен. 90 | 2. Перейдите на закладку SETTINGS 91 | 3. Введите https://github.com/volshebniks/sauresha и выберите категорию Integration, нажмите Сохранить 92 | 4. Новый репозиторий Integration Saures controllers with HA будет добавлен на закладке Integration 93 | 5. Устновите SauresHA из него 94 | 3. Осуществляем конфигурацию компонента в Home Assistant через GUI. 95 | 4. Перезапустите HA. 96 | 97 | # План развития проекта 98 | - [X] Добавить проект в HACS 99 | - [ ] Сделать сенсоры для счетчиков с показаниями за день/месяц/год 100 | - [X] Добавить управление кранами 101 | - [ ] Сделать pallete для Node-Red 102 | - [X] Сделать полноценную интеграцию с Home Assistant (добавляется в раздел интеграции) 103 | 104 | 105 | # Credits 106 | 107 | Большое спасибо следующим организациям и проектам, работа которых имеет важное значение для развития проекта: 108 | 109 | Нет их пока :) 110 | 111 | ---------------------------------------------------------------------------------------------------------------------------------- 112 | Пожертвование на развитие проекта [Яндекс.Деньги](https://money.yandex.ru/to/41001566881198) -------------------------------------------------------------------------------- /custom_components/sauresha/README.md: -------------------------------------------------------------------------------- 1 | # SauresHA 2 | [![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg)](https://github.com/custom-components/hacs) 3 | [![GitHub release (latest by date)](https://img.shields.io/github/v/release/volshebniks/sauresha)](https://github.com/volshebniks/sauresha/releases) 4 | ![GitHub Release Date](https://img.shields.io/github/release-date/volshebniks/sauresha) 5 | [![GitHub](https://img.shields.io/github/license/volshebniks/sauresha)](LICENSE) 6 | 7 | [![Maintenance](https://img.shields.io/badge/Maintained%3F-Yes-brightgreen.svg)](https://github.com/volshebniks/sauresha/graphs/commit-activity) 8 | [![GitHub issues](https://img.shields.io/github/issues/volshebniks/sauresha)](https://github.com/volshebniks/sauresha/issues) 9 | 10 | [![Donate](https://img.shields.io/badge/donate-Coffee-yellow.svg)](https://https://www.buymeacoffee.com/RlnBV9r) 11 | [![Donate](https://img.shields.io/badge/donate-Yandex-red.svg)](https://money.yandex.ru/to/41001566881198) 12 | 13 | Пожертвование на развитие проекта [Яндекс.Деньги](https://money.yandex.ru/to/41001566881198) 14 | 15 | ## Update 1: Начиная с версии 0.3: 16 | * сделан переход на новое клиентское API 17 | * добавлена необязательная настройка для sensor - scan_interval. Время обновления в минутах. По умолчанию = 10 минут. 18 | * из-за перехода появились новые атрибуты у сенсоров. 19 | 20 | ## Update 2: версия 0.3.5: 21 | * уменьшено количество вызово API 22 | * ускорена первоначальная инициализация модуля 23 | * сделан переход на асинхронные методы 24 | * исправлена ошибка с заднием своего scan_interval 25 | * исправлена ошибка связанная с наличием русских букв в серийных номерах 26 | * на стороне Saures явно починили кеширование для нового API 27 | 28 | ## Update 3: версия 0.3.8: 29 | * вернул синхронные вызовы 30 | 31 | ## Update 4: Начиная с версии 0.5: 32 | * Существенно сокращено кол-во обращений к серверу Saures, для предотвращения блокировки. 33 |
Рекомендую в настройках указать: 34 | ```yaml 35 | scan_interval: 36 | minutes: 30 37 | ``` 38 | Иначе могут быть блокировки в будущем. 39 | 40 | ## Update 5: Начиная с версии 0.6: 41 | * значительно изменен механизм настройки 42 | * можно задавать свои мена для всего 43 | * можно в настройках делать ссылки на !secret 44 | * добавил в manifest, version 45 | 46 | ## Update 6: Начиная с версии 1.0: 47 | * полностью переработан код для минимизации обращений к серверу Saures 48 | * внедрен асинхронный режим работы 49 | * Добавлено управление кранами 50 | * настройка через GUI 51 | 52 | ## Содержание 53 | 54 | * [Установка](#устнановка) 55 | * [Ручная установка](#ручная-установка) 56 | * [Установка через HACS](#hacs_установка) 57 | 58 | Для связи: 59 | 60 | Интеграция котроллеров [Saures](https://www.saures.ru) c [Home Assistant](https://www.home-assistant.io/) 61 | # Описание 62 | 63 | В настоящее время поддерживаются следующие типы устройств от Saurus 64 | 1. Счетчик холодной воды (м³) = sensor в Home Assistant 65 | 2. Счетчик горячей воды (м³) = sensor в Home Assistant 66 | 3. Счетчик газа (м³) = sensor в Home Assistant 67 | 4. Датчик протечки (0 – нет протечки, 1 - протечка) = binary_sensor в Home Assistant 68 | 5. Датчик температуры (градусы) = sensor в Home Assistant 69 | 6. Электро-шаровой кран управление (0 – открыться, 1 - закрыться) - поддерживается, switch в Home Assistant 70 | 7. Счетчик тепла (кВт*ч) = sensor в Home Assistant 71 | 8. Счетчик электричества (кВт*ч) (в том числе многотарифные) = sensor в Home Assistant 72 | 9. Сухой контакт (0 – деактивирован, 1 – активирован) = binary_sensor в Home Assistant 73 | 10. Электро-шаровой кран состояние (0 – не подключен модуль, 1 – неизвестное состояние, 2 – открыт, 3 - закрыт) = sensor в Home Assistant 74 | 11. Непосредственно сами контроллеры = sensor в Home Assistant 75 | 76 | ## Установка 77 | 78 | ### Ручная установка 79 | 80 | 1. Добавляем компонент в Home Assistant 81 | Распаковываем архив. Папку sauresha берем целиком и копируем в custom_components. 82 | 2. Осуществляем конфигурацию компонента в Home Assistant через GUI. 83 | 3. Перезагружаем HA 84 | 85 | ### HACS установка 86 | 87 | 1. Убедитесь, что [HACS](https://custom-components.github.io/hacs/) уже устновлен. 88 | 2. Перейдите на закладку SETTINGS 89 | 3. Введите https://github.com/volshebniks/sauresha и выберите категорию Integration, нажмите Сохранить 90 | 4. Новый репозиторий Integration Saures controllers with HA будет добавлен на закладке Integration 91 | 5. Устновите SauresHA из него 92 | 3. Осуществляем конфигурацию компонента в Home Assistant через GUI. 93 | 4. Перезапустите HA. 94 | 95 | # План развития проекта 96 | - [X] Добавить проект в HACS 97 | - [ ] Сделать сенсоры для счетчиков с показаниями за день/месяц/год 98 | - [X] Добавить управление кранами 99 | - [ ] Сделать pallete для Node-Red 100 | - [X] Сделать полноценную интеграцию с Home Assistant (добавляется в раздел интеграции) 101 | 102 | 103 | # Credits 104 | 105 | Большое спасибо следующим организациям и проектам, работа которых имеет важное значение для развития проекта: 106 | 107 | Нет их пока :) 108 | 109 | ---------------------------------------------------------------------------------------------------------------------------------- 110 | Пожертвование на развитие проекта [Яндекс.Деньги](https://money.yandex.ru/to/41001566881198) -------------------------------------------------------------------------------- /custom_components/sauresha/info.md: -------------------------------------------------------------------------------- 1 | # SauresHA 2 | [![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg)](https://github.com/custom-components/hacs) 3 | [![GitHub release (latest by date)](https://img.shields.io/github/v/release/volshebniks/sauresha)](https://github.com/volshebniks/sauresha/releases) 4 | ![GitHub Release Date](https://img.shields.io/github/release-date/volshebniks/sauresha) 5 | [![GitHub](https://img.shields.io/github/license/volshebniks/sauresha)](LICENSE) 6 | 7 | [![Maintenance](https://img.shields.io/badge/Maintained%3F-Yes-brightgreen.svg)](https://github.com/volshebniks/sauresha/graphs/commit-activity) 8 | [![GitHub issues](https://img.shields.io/github/issues/volshebniks/sauresha)](https://github.com/volshebniks/sauresha/issues) 9 | 10 | [![Donate](https://img.shields.io/badge/donate-Coffee-yellow.svg)](https://https://www.buymeacoffee.com/RlnBV9r) 11 | [![Donate](https://img.shields.io/badge/donate-Yandex-red.svg)](https://money.yandex.ru/to/41001566881198) 12 | 13 | Пожертвование на развитие проекта [Яндекс.Деньги](https://money.yandex.ru/to/41001566881198) 14 | 15 | ## Update 1: Начиная с версии 0.3: 16 | * сделан переход на новое клиентское API 17 | * добавлена необязательная настройка для sensor - scan_interval. Время обновления в минутах. По умолчанию = 10 минут. 18 | * из-за перехода появились новые атрибуты у сенсоров. 19 | 20 | ## Update 2: версия 0.3.5: 21 | * уменьшено количество вызово API 22 | * ускорена первоначальная инициализация модуля 23 | * сделан переход на асинхронные методы 24 | * исправлена ошибка с заднием своего scan_interval 25 | * исправлена ошибка связанная с наличием русских букв в серийных номерах 26 | * на стороне Saures явно починили кеширование для нового API 27 | 28 | ## Update 3: версия 0.3.8: 29 | * вернул синхронные вызовы 30 | 31 | ## Update 4: Начиная с версии 0.5: 32 | * Существенно сокращено кол-во обращений к серверу Saures, для предотвращения блокировки. 33 |
Рекомендую в настройках указать: 34 | ```yaml 35 | scan_interval: 36 | minutes: 30 37 | ``` 38 | Иначе могут быть блокировки в будущем. 39 | 40 | ## Update 5: Начиная с версии 0.6: 41 | * значительно изменен механизм настройки 42 | * можно задавать свои мена для всего 43 | * можно в настройках делать ссылки на !secret 44 | * добавил в manifest, version 45 | 46 | ## Update 6: Начиная с версии 1.0: 47 | * полностью переработан код для минимизации обращений к серверу Saures 48 | * внедрен асинхронный режим работы 49 | * Добавлено управление кранами 50 | * настройка через GUI 51 | 52 | ## Содержание 53 | 54 | * [Установка](#устнановка) 55 | * [Ручная установка](#ручная-установка) 56 | * [Установка через HACS](#hacs_установка) 57 | 58 | 59 | 60 | Для связи: 61 | 62 | Интеграция котроллеров [Saures](https://www.saures.ru) c [Home Assistant](https://www.home-assistant.io/) 63 | # Описание 64 | 65 | В настоящее время поддерживаются следующие типы устройств от Saurus 66 | 1. Счетчик холодной воды (м³) = sensor в Home Assistant 67 | 2. Счетчик горячей воды (м³) = sensor в Home Assistant 68 | 3. Счетчик газа (м³) = sensor в Home Assistant 69 | 4. Датчик протечки (0 – нет протечки, 1 - протечка) = binary_sensor в Home Assistant 70 | 5. Датчик температуры (градусы) = sensor в Home Assistant 71 | 6. Электро-шаровой кран управление (0 – открыться, 1 - закрыться) - поддерживается, switch в Home Assistant 72 | 7. Счетчик тепла (кВт*ч) = sensor в Home Assistant 73 | 8. Счетчик электричества (кВт*ч) (в том числе многотарифные) = sensor в Home Assistant 74 | 9. Сухой контакт (0 – деактивирован, 1 – активирован) = binary_sensor в Home Assistant 75 | 10. Электро-шаровой кран состояние (0 – не подключен модуль, 1 – неизвестное состояние, 2 – открыт, 3 - закрыт) = sensor в Home Assistant 76 | 11. Непосредственно сами контроллеры = sensor в Home Assistant 77 | 78 | ## Установка 79 | 80 | ### Ручная установка 81 | 82 | 1. Добавляем компонент в Home Assistant 83 | Распаковываем архив. Папку sauresha берем целиком и копируем в custom_components. 84 | 2. Осуществляем конфигурацию компонента в Home Assistant через GUI. 85 | 3. Перезагружаем HA 86 | 87 | ### HACS установка 88 | 89 | 1. Убедитесь, что [HACS](https://custom-components.github.io/hacs/) уже устновлен. 90 | 2. Перейдите на закладку SETTINGS 91 | 3. Введите https://github.com/volshebniks/sauresha и выберите категорию Integration, нажмите Сохранить 92 | 4. Новый репозиторий Integration Saures controllers with HA будет добавлен на закладке Integration 93 | 5. Устновите SauresHA из него 94 | 3. Осуществляем конфигурацию компонента в Home Assistant через GUI. 95 | 4. Перезапустите HA. 96 | 97 | # План развития проекта 98 | - [X] Добавить проект в HACS 99 | - [ ] Сделать сенсоры для счетчиков с показаниями за день/месяц/год 100 | - [X] Добавить управление кранами 101 | - [ ] Сделать pallete для Node-Red 102 | - [X] Сделать полноценную интеграцию с Home Assistant (добавляется в раздел интеграции) 103 | 104 | 105 | # Credits 106 | 107 | Большое спасибо следующим организациям и проектам, работа которых имеет важное значение для развития проекта: 108 | 109 | Нет их пока :) 110 | 111 | ---------------------------------------------------------------------------------------------------------------------------------- 112 | Пожертвование на развитие проекта [Яндекс.Деньги](https://money.yandex.ru/to/41001566881198) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | *.pyproj 10 | *.sln 11 | *.pyproj.user 12 | *.7z 13 | 14 | # User-specific files (MonoDevelop/Xamarin Studio) 15 | *.userprefs 16 | 17 | # Build results 18 | [Dd]ebug/ 19 | [Dd]ebugPublic/ 20 | [Rr]elease/ 21 | [Rr]eleases/ 22 | x64/ 23 | x86/ 24 | bld/ 25 | [Bb]in/ 26 | [Oo]bj/ 27 | [Ll]og/ 28 | 29 | # Visual Studio 2015 cache/options directory 30 | .vs/ 31 | # Uncomment if you have tasks that create the project's static files in wwwroot 32 | #wwwroot/ 33 | 34 | # MSTest test Results 35 | [Tt]est[Rr]esult*/ 36 | [Bb]uild[Ll]og.* 37 | 38 | # NUNIT 39 | *.VisualState.xml 40 | TestResult.xml 41 | 42 | # Build Results of an ATL Project 43 | [Dd]ebugPS/ 44 | [Rr]eleasePS/ 45 | dlldata.c 46 | 47 | # DNX 48 | project.lock.json 49 | project.fragment.lock.json 50 | artifacts/ 51 | 52 | *_i.c 53 | *_p.c 54 | *_i.h 55 | *.ilk 56 | *.meta 57 | *.obj 58 | *.pch 59 | *.pdb 60 | *.pgc 61 | *.pgd 62 | *.rsp 63 | *.sbr 64 | *.tlb 65 | *.tli 66 | *.tlh 67 | *.tmp 68 | *.tmp_proj 69 | *.log 70 | *.vspscc 71 | *.vssscc 72 | .builds 73 | *.pidb 74 | *.svclog 75 | *.scc 76 | 77 | # Chutzpah Test files 78 | _Chutzpah* 79 | 80 | # Visual C++ cache files 81 | ipch/ 82 | *.aps 83 | *.ncb 84 | *.opendb 85 | *.opensdf 86 | *.sdf 87 | *.cachefile 88 | *.VC.db 89 | *.VC.VC.opendb 90 | 91 | # Visual Studio profiler 92 | *.psess 93 | *.vsp 94 | *.vspx 95 | *.sap 96 | 97 | # TFS 2012 Local Workspace 98 | $tf/ 99 | 100 | # Guidance Automation Toolkit 101 | *.gpState 102 | 103 | # ReSharper is a .NET coding add-in 104 | _ReSharper*/ 105 | *.[Rr]e[Ss]harper 106 | *.DotSettings.user 107 | 108 | # JustCode is a .NET coding add-in 109 | .JustCode 110 | 111 | # TeamCity is a build add-in 112 | _TeamCity* 113 | 114 | # DotCover is a Code Coverage Tool 115 | *.dotCover 116 | 117 | # NCrunch 118 | _NCrunch_* 119 | .*crunch*.local.xml 120 | nCrunchTemp_* 121 | 122 | # MightyMoose 123 | *.mm.* 124 | AutoTest.Net/ 125 | 126 | # Web workbench (sass) 127 | .sass-cache/ 128 | 129 | # Installshield output folder 130 | [Ee]xpress/ 131 | 132 | # DocProject is a documentation generator add-in 133 | DocProject/buildhelp/ 134 | DocProject/Help/*.HxT 135 | DocProject/Help/*.HxC 136 | DocProject/Help/*.hhc 137 | DocProject/Help/*.hhk 138 | DocProject/Help/*.hhp 139 | DocProject/Help/Html2 140 | DocProject/Help/html 141 | 142 | # Click-Once directory 143 | publish/ 144 | 145 | # Publish Web Output 146 | *.[Pp]ublish.xml 147 | *.azurePubxml 148 | # TODO: Comment the next line if you want to checkin your web deploy settings 149 | # but database connection strings (with potential passwords) will be unencrypted 150 | #*.pubxml 151 | *.publishproj 152 | 153 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 154 | # checkin your Azure Web App publish settings, but sensitive information contained 155 | # in these scripts will be unencrypted 156 | PublishScripts/ 157 | 158 | # NuGet Packages 159 | *.nupkg 160 | # The packages folder can be ignored because of Package Restore 161 | **/packages/* 162 | # except build/, which is used as an MSBuild target. 163 | !**/packages/build/ 164 | # Uncomment if necessary however generally it will be regenerated when needed 165 | #!**/packages/repositories.config 166 | # NuGet v3's project.json files produces more ignoreable files 167 | *.nuget.props 168 | *.nuget.targets 169 | 170 | # Microsoft Azure Build Output 171 | csx/ 172 | *.build.csdef 173 | 174 | # Microsoft Azure Emulator 175 | ecf/ 176 | rcf/ 177 | 178 | # Windows Store app package directories and files 179 | AppPackages/ 180 | BundleArtifacts/ 181 | Package.StoreAssociation.xml 182 | _pkginfo.txt 183 | 184 | # Visual Studio cache files 185 | # files ending in .cache can be ignored 186 | *.[Cc]ache 187 | # but keep track of directories ending in .cache 188 | !*.[Cc]ache/ 189 | 190 | # Others 191 | ClientBin/ 192 | ~$* 193 | *~ 194 | *.dbmdl 195 | *.dbproj.schemaview 196 | *.jfm 197 | *.pfx 198 | *.publishsettings 199 | node_modules/ 200 | orleans.codegen.cs 201 | 202 | # Since there are multiple workflows, uncomment next line to ignore bower_components 203 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 204 | #bower_components/ 205 | 206 | # RIA/Silverlight projects 207 | Generated_Code/ 208 | 209 | # Backup & report files from converting an old project file 210 | # to a newer Visual Studio version. Backup files are not needed, 211 | # because we have git ;-) 212 | _UpgradeReport_Files/ 213 | Backup*/ 214 | UpgradeLog*.XML 215 | UpgradeLog*.htm 216 | 217 | # SQL Server files 218 | *.mdf 219 | *.ldf 220 | 221 | # Business Intelligence projects 222 | *.rdl.data 223 | *.bim.layout 224 | *.bim_*.settings 225 | 226 | # Microsoft Fakes 227 | FakesAssemblies/ 228 | 229 | # GhostDoc plugin setting file 230 | *.GhostDoc.xml 231 | 232 | # Node.js Tools for Visual Studio 233 | .ntvs_analysis.dat 234 | 235 | # Visual Studio 6 build log 236 | *.plg 237 | 238 | # Visual Studio 6 workspace options file 239 | *.opt 240 | 241 | # Visual Studio LightSwitch build output 242 | **/*.HTMLClient/GeneratedArtifacts 243 | **/*.DesktopClient/GeneratedArtifacts 244 | **/*.DesktopClient/ModelManifest.xml 245 | **/*.Server/GeneratedArtifacts 246 | **/*.Server/ModelManifest.xml 247 | _Pvt_Extensions 248 | 249 | # Paket dependency manager 250 | .paket/paket.exe 251 | paket-files/ 252 | 253 | # FAKE - F# Make 254 | .fake/ 255 | 256 | # JetBrains Rider 257 | .idea/ 258 | *.sln.iml 259 | 260 | # CodeRush 261 | .cr/ 262 | 263 | # Python Tools for Visual Studio (PTVS) 264 | __pycache__/ 265 | *.pyc 266 | /.vscode 267 | *.zip 268 | /custom_components/sauresha/LICENSE 269 | custom_components/sauresha/.vscode/settings.json 270 | .gitignore 271 | -------------------------------------------------------------------------------- /custom_components/sauresha/config_flow.py: -------------------------------------------------------------------------------- 1 | import voluptuous as vol 2 | import logging 3 | import uuid 4 | from homeassistant.config_entries import ( 5 | ConfigFlow, 6 | ConfigEntry, 7 | OptionsFlow, 8 | CONN_CLASS_LOCAL_POLL, 9 | ) 10 | import homeassistant.helpers.config_validation as cv 11 | from .const import DOMAIN, CONF_FLATS 12 | from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_SCAN_INTERVAL 13 | from homeassistant.core import callback 14 | from .api import SauresHA 15 | 16 | _LOGGER = logging.getLogger(__name__) 17 | 18 | 19 | # --------------------------- 20 | # configured_instances 21 | # --------------------------- 22 | class SaureshaConfigFlow(ConfigFlow, domain=DOMAIN): 23 | 24 | VERSION = 1 25 | CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL 26 | 27 | def __init__(self): 28 | """Init config flow.""" 29 | self._errors = {} 30 | 31 | async def async_step_import(self, platform_config): 32 | if platform_config is None: 33 | return self.async_abort(reason="unknown_error") 34 | 35 | await self.async_set_unique_id(platform_config[CONF_EMAIL]) 36 | self._abort_if_unique_id_configured() 37 | 38 | # if self._async_current_entries(): 39 | # return self.async_abort(reason="no_mixed_config") 40 | 41 | email = platform_config[CONF_EMAIL] 42 | password = platform_config[CONF_PASSWORD] 43 | flat_ids = platform_config[CONF_FLATS] 44 | user_input = { 45 | CONF_EMAIL: email, 46 | CONF_PASSWORD: password, 47 | CONF_SCAN_INTERVAL: 30, 48 | } 49 | user_options = { 50 | CONF_FLATS: str(flat_ids).split(","), 51 | } 52 | cur_entry = self.async_create_entry( 53 | title=platform_config[CONF_EMAIL], data=user_input, options=user_options 54 | ) 55 | return cur_entry 56 | 57 | async def async_step_user(self, user_input=None): 58 | 59 | self._errors = {} 60 | 61 | # if self._async_current_entries(): 62 | # return self.async_abort(reason="no_mixed_config") 63 | # if self.hass.data.get(DOMAIN): 64 | # return self.async_abort(reason="no_mixed_config") 65 | 66 | if user_input is not None: 67 | 68 | # Test connection 69 | SauresAPI: SauresHA = SauresHA( 70 | self.hass, user_input[CONF_EMAIL], user_input[CONF_PASSWORD], "true", "" 71 | ) 72 | 73 | res = await SauresAPI.auth() 74 | if not res: 75 | self._errors["base"] = "cannot_connect" 76 | 77 | # Save instance 78 | if not self._errors: 79 | return self.async_create_entry( 80 | title=user_input[CONF_EMAIL], data=user_input 81 | ) 82 | 83 | return self._show_config_form(user_input, self._errors) 84 | 85 | return self._show_config_form( 86 | user_input={ 87 | CONF_EMAIL: "email", 88 | CONF_PASSWORD: "pass", 89 | CONF_SCAN_INTERVAL: 30, 90 | }, 91 | current_errors=self._errors, 92 | ) 93 | 94 | # --------------------------- 95 | # _show_config_form 96 | # --------------------------- 97 | def _show_config_form(self, user_input, current_errors=None): 98 | """Show the configuration form to edit data.""" 99 | return self.async_show_form( 100 | step_id="user", 101 | data_schema=vol.Schema( 102 | { 103 | vol.Required(CONF_EMAIL, default=user_input[CONF_EMAIL]): str, 104 | vol.Required(CONF_PASSWORD, default=user_input[CONF_PASSWORD]): str, 105 | vol.Required( 106 | CONF_SCAN_INTERVAL, default=user_input[CONF_SCAN_INTERVAL] 107 | ): int, 108 | } 109 | ), 110 | errors=current_errors, 111 | ) 112 | 113 | @staticmethod 114 | @callback 115 | def async_get_options_flow(config_entry): 116 | """Get component options flow.""" 117 | return SaureshaOptionsFlowHandler(config_entry) 118 | 119 | 120 | class SaureshaOptionsFlowHandler(OptionsFlow): 121 | def __init__(self, entry: ConfigEntry): 122 | 123 | self._entry = dict(entry.options) 124 | self._data = dict(entry.data) 125 | self._data["unique_id"] = str(uuid.uuid4()) 126 | self._errors = {} 127 | 128 | async def async_step_init(self, user_input=None): 129 | self._errors = {} 130 | 131 | if user_input is not None: 132 | return self.async_create_entry( 133 | title=self._data[CONF_EMAIL], data=user_input 134 | ) 135 | 136 | try: 137 | SauresAPI: SauresHA = SauresHA( 138 | self.hass, self._data[CONF_EMAIL], self._data[CONF_PASSWORD], "true", "" 139 | ) 140 | res = await SauresAPI.async_get_flats(self.hass) 141 | if not res: 142 | self._errors["base"] = "cannot_connect" 143 | except Exception: 144 | res = {} 145 | 146 | ress = self._show_options_form( 147 | user_input={CONF_FLATS: self._entry}, current_errors=self._errors, flats=res 148 | ) 149 | 150 | return ress 151 | 152 | # --------------------------- 153 | # _show_options_form 154 | # --------------------------- 155 | def _show_options_form(self, user_input, current_errors=None, flats=None): 156 | """Show the configuration form to edit data.""" 157 | selected_flats = list 158 | all_flats = {} 159 | if user_input[CONF_FLATS]: 160 | selected_flats = user_input[CONF_FLATS][CONF_FLATS] 161 | 162 | for flat in flats: 163 | all_flats[str(flat)] = f"{flats[flat]} ({flat})" 164 | return self.async_show_form( 165 | step_id="init", 166 | data_schema=vol.Schema( 167 | { 168 | vol.Optional( 169 | CONF_FLATS, 170 | default=selected_flats, 171 | ): cv.multi_select(all_flats) 172 | }, 173 | extra=vol.ALLOW_EXTRA, 174 | ), 175 | errors=current_errors, 176 | ) 177 | -------------------------------------------------------------------------------- /custom_components/sauresha/api.py: -------------------------------------------------------------------------------- 1 | """API for Saures""" 2 | import logging 3 | import time 4 | import datetime 5 | import functools 6 | import asyncio 7 | from homeassistant.helpers.aiohttp_client import async_get_clientsession 8 | from .classes import SauresController, SauresSensor 9 | from .const import CONF_BINARY_SENSORS_DEF, CONF_SWITCH_DEF 10 | 11 | _LOGGER = logging.getLogger(__name__) 12 | 13 | 14 | class SauresHA: 15 | _sid: str 16 | _debug: bool 17 | _last_login_time: time 18 | _binarysensors: dict 19 | _sensors: dict 20 | _switches: dict 21 | _data: dict 22 | _flats: list 23 | _hass: object 24 | 25 | def __init__(self, hass, email, password, is_debug, userflats): 26 | self._email = email 27 | self._password = password 28 | self._debug = is_debug 29 | self._last_login_time = datetime.datetime(2000, 1, 1, 1, 1, 1) 30 | self._last_update_time_dict = {} 31 | self._data = dict() 32 | self._sensors = dict() 33 | self._controllers = dict() 34 | self._binarysensors = dict() 35 | self._switches = dict() 36 | self._flats = list() 37 | self.userflats = userflats 38 | self._hass = hass 39 | self._sid_renewal = False 40 | 41 | def checkflatsfilter(self, filter_flats, flat_id): 42 | if len(filter_flats) == 0: 43 | return True 44 | 45 | for i in filter_flats: 46 | try: 47 | if i == str(flat_id): 48 | return True 49 | except: 50 | pass 51 | return False 52 | 53 | @property 54 | def flats(self): 55 | return self._flats 56 | 57 | async def auth(self) -> bool: 58 | bln_return = False 59 | try: 60 | now = datetime.datetime.now() 61 | period = now - self._last_login_time 62 | if (period.total_seconds() / 60) >= 5: 63 | clientsession = async_get_clientsession(self._hass) 64 | self._last_login_time = datetime.datetime.now() 65 | if self._sid_renewal: 66 | return False 67 | self._sid_renewal = True 68 | auth_data = await clientsession.post( 69 | "https://api.saures.ru/1.0/login", 70 | data={"email": self._email, "password": self._password}, 71 | headers={"user-agent": "chrome"}, 72 | ) 73 | 74 | result = await auth_data.json() 75 | if not result: 76 | raise Exception("Invalid credentials") 77 | if len(result["errors"]) > 0: 78 | raise Exception(result["errors"][0]["msg"]) 79 | self._sid = result["data"]["sid"] 80 | bln_return = result["status"] != "bad" 81 | else: 82 | self._sid_renewal = False 83 | if self._sid == "": 84 | bln_return = False 85 | else: 86 | bln_return = True 87 | 88 | except Exception as err: # catch *all* exceptions 89 | self._sid_renewal = False 90 | _LOGGER.error(str(err)) 91 | 92 | return bln_return 93 | 94 | async def async_get_flats(self, hass): 95 | self._flats = dict() 96 | clientsession = async_get_clientsession(hass) 97 | if len(self.userflats) == 0: 98 | try: 99 | lock = asyncio.Lock() 100 | async with lock: 101 | auth_data = await self.auth() 102 | if auth_data: 103 | flats_data = await clientsession.get( 104 | "https://api.saures.ru/1.0/user/objects", 105 | params={"sid": self._sid}, 106 | headers={"user-agent": "chrome"}, 107 | ) 108 | 109 | result = await flats_data.json() 110 | result_data = result["data"]["objects"] 111 | self._flats.clear() 112 | for val in result_data: 113 | self._flats[ 114 | val.get("id") 115 | ] = f"{val.get('label')}:{val.get('house')}:{val.get('number')}" 116 | except Exception as err: 117 | if self._debug: 118 | _LOGGER.error(str(err)) 119 | else: 120 | self._flats = self.userflats 121 | 122 | return self._flats 123 | 124 | def checkdict(self, data, value): 125 | for i in data.keys(): 126 | try: 127 | if i == value: 128 | return True 129 | except: 130 | pass 131 | return False 132 | 133 | # 1.3,1.4,1.5 - счетчик C1 134 | # 3.1,3.2 - контроллер R1 4 аналоговых канала (снят с продаж в 2017 году) 135 | # 3.4 - контроллер R1 8 аналоговых каналов (снят с продаж в 2018 году) 136 | # 3.5 - контроллер R1 4 аналоговых канала 137 | # 4.0 - контроллер R2 8 аналоговых каналов 138 | # 4.1 - контроллер R4 2 аналоговых канала и 8 цифровых каналов 139 | # 6.3 - контроллер R5 8 аналоговых каналов и 8 цифровых каналов 140 | # 7.2 - контроллер R6 8 аналоговых каналов и 32 цифровых каналов 141 | # 8.2 - контроллер R7 4 аналоговых канала и 32 цифровых каналов (снят с продаж в 2020 году) 142 | # 8.3 - контроллер R7 4 аналоговых канала и 32 цифровых каналов 143 | # 9.1 - контроллер R8(после 2022) 144 | def get_controller_name(self, version_id): 145 | return { 146 | version_id == "1.3" 147 | or version_id == "1.4" 148 | or version_id == "1.5": "счетчик C1", 149 | version_id == "3.1" or version_id == "3.2": "контроллер R1(до 2017)", 150 | version_id == "3.4": "контроллер R1 8 (2017-2018)", 151 | version_id == "3.5": "контроллер R1 4 (после 2018))", 152 | version_id == "4.0": "контроллер R2", 153 | version_id == "4.1": "контроллер R4", 154 | version_id == "6.3": "контроллер R5", 155 | version_id == "7.2": "контроллер R6", 156 | version_id == "8.2": "контроллер R7(до 2020)", 157 | version_id == "8.3": "контроллер R7(после 2020)", 158 | version_id == "9.1": "контроллер R8(после 2022)", 159 | }[True] 160 | 161 | async def set_command(self, meter_id, command_text): 162 | bln_return = False 163 | try: 164 | clientsession = async_get_clientsession(self._hass) 165 | lock = asyncio.Lock() 166 | async with lock: 167 | auth_data = await self.auth() 168 | if auth_data: 169 | self._last_login_time = datetime.datetime.now() 170 | res_data = await clientsession.post( 171 | "https://api.saures.ru//1.0/meter/control", 172 | data={"sid": self._sid, "id": meter_id, "command": command_text}, 173 | headers={"user-agent": "chrome"}, 174 | ) 175 | result = await res_data.json() 176 | if not result: 177 | raise Exception("Ошибка выполнения комманды.") 178 | 179 | bln_return = result["status"] != "bad" 180 | if not bln_return: 181 | msg = f'Ошибка выполнения комманды - command: {command_text} ,meter_id: {meter_id}, ошибка: {result["errors"][0]["msg"]}.' 182 | _LOGGER.warning(msg) 183 | 184 | except Exception as err: # catch *all* exceptions 185 | if self._debug: 186 | _LOGGER.error(str(err)) 187 | 188 | return bln_return 189 | 190 | async def async_get_data(self, flat_id, reload=False): 191 | now = datetime.datetime.now() 192 | if not self.checkdict(self._last_update_time_dict, flat_id): 193 | self._last_update_time_dict[flat_id] = datetime.datetime( 194 | 2000, 1, 1, 1, 1, 1 195 | ) 196 | period = now - self._last_update_time_dict[flat_id] 197 | if (period.total_seconds() / 60) >= 5 or reload: 198 | self._last_update_time_dict[flat_id] = datetime.datetime.now() 199 | lock = asyncio.Lock() 200 | async with lock: 201 | auth_data = await self.auth() 202 | if auth_data: 203 | try: 204 | clientsession = async_get_clientsession(self._hass) 205 | controllers = await clientsession.get( 206 | "https://api.saures.ru/1.0/object/meters", 207 | params={"id": str(flat_id), "sid": self._sid}, 208 | headers={"user-agent": "chrome"}, 209 | ) 210 | if controllers.status == 200: 211 | data = await controllers.json(content_type=None) 212 | self._data[flat_id] = data["data"]["sensors"] 213 | except Exception as err: 214 | if self._debug: 215 | _LOGGER.error(str(err)) 216 | 217 | return self._data[flat_id] 218 | 219 | async def async_get_controllers(self, flat_id): 220 | lock = asyncio.Lock() 221 | async with lock: 222 | controllers = await self.async_get_data(flat_id) 223 | 224 | self._controllers[flat_id] = controllers 225 | return self._controllers[flat_id] 226 | 227 | def get_controller(self, flat_id, sn): 228 | controllers = self._controllers[flat_id] 229 | return next( 230 | ( 231 | SauresController(controller) 232 | for controller in controllers 233 | if controller["sn"] == sn 234 | ), 235 | SauresController(dict()), 236 | ) 237 | 238 | async def async_get_binary_sensors(self, flat_id): 239 | results = list() 240 | lock = asyncio.Lock() 241 | async with lock: 242 | meters = await self.async_get_data(flat_id) 243 | res = functools.reduce( 244 | list.__add__, map(lambda sensor: sensor["meters"], meters) 245 | ) 246 | for obj in res: 247 | objtype = obj.get("type", {}).get("number") 248 | if objtype in CONF_BINARY_SENSORS_DEF: 249 | results.append(obj) 250 | 251 | self._binarysensors[flat_id] = results 252 | 253 | return self._binarysensors[flat_id] 254 | 255 | async def async_get_sensors(self, flat_id): 256 | results = list() 257 | lock = asyncio.Lock() 258 | async with lock: 259 | meters = await self.async_get_data(flat_id) 260 | 261 | if meters: 262 | res = functools.reduce( 263 | list.__add__, map(lambda sensor: sensor["meters"], meters) 264 | ) 265 | for obj in res: 266 | objtype = obj.get("type", {}).get("number") 267 | if ( 268 | objtype not in CONF_BINARY_SENSORS_DEF 269 | and objtype not in CONF_SWITCH_DEF 270 | ): 271 | results.append(obj) 272 | 273 | self._sensors[flat_id] = results 274 | 275 | return self._sensors[flat_id] 276 | 277 | def get_sensor(self, flat_id, sensor_id): 278 | if flat_id in self._sensors: 279 | meters = self._sensors[flat_id] 280 | for obj in meters: 281 | if obj["meter_id"] == sensor_id: 282 | return SauresSensor(obj) 283 | return SauresSensor(dict()) 284 | 285 | def get_binarysensor(self, flat_id, sensor_id): 286 | if flat_id in self._binarysensors: 287 | meters = self._binarysensors[flat_id] 288 | for obj in meters: 289 | if obj["meter_id"] == sensor_id: 290 | return SauresSensor(obj) 291 | 292 | return SauresSensor(dict()) 293 | 294 | async def async_get_switches(self, flat_id, reload): 295 | results = list() 296 | lock = asyncio.Lock() 297 | async with lock: 298 | meters = await self.async_get_data(flat_id) 299 | res = functools.reduce( 300 | list.__add__, map(lambda sensor: sensor["meters"], meters) 301 | ) 302 | for obj in res: 303 | if obj.get("type", {}).get("number") in CONF_SWITCH_DEF: 304 | results.append(obj) 305 | 306 | self._switches[flat_id] = results 307 | return self._switches[flat_id] 308 | 309 | def get_switch(self, flat_id, switch_id): 310 | if flat_id in self._switches: 311 | meters = self._switches[flat_id] 312 | for obj in meters: 313 | if obj["meter_id"] == switch_id: 314 | return SauresSensor(obj) 315 | return SauresSensor(dict()) 316 | 317 | async def async_fetch_data(self): 318 | try: 319 | lock = asyncio.Lock() 320 | async with lock: 321 | auth_data = await self.auth() 322 | if auth_data: 323 | flats = await self.async_get_flats(self._hass) 324 | self._flats = flats 325 | for curflat in flats: 326 | try: 327 | await self.async_get_controllers(curflat) 328 | await self.async_get_sensors(curflat) 329 | await self.async_get_binary_sensors(curflat) 330 | await self.async_get_switches(curflat, False) 331 | await asyncio.sleep(10) 332 | except Exception: 333 | _LOGGER.exception("Error load data") 334 | except Exception: 335 | _LOGGER.exception("Error load data") 336 | -------------------------------------------------------------------------------- /custom_components/sauresha/entity.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provides entities 3 | """ 4 | 5 | import logging 6 | import datetime 7 | import asyncio 8 | from datetime import timedelta 9 | from homeassistant.helpers.entity import Entity, DeviceInfo 10 | from homeassistant.components.switch import SwitchEntity 11 | from homeassistant.components.binary_sensor import BinarySensorDeviceClass 12 | from homeassistant.helpers.event import async_track_time_interval 13 | from homeassistant.util import slugify 14 | from homeassistant.const import ATTR_BATTERY_LEVEL 15 | 16 | from .api import SauresHA 17 | from .const import ( 18 | CONF_COMMAND_ACTIVATE, 19 | CONF_COMMAND_DEACTIVATE, 20 | DOMAIN, 21 | CONF_BINARY_SENSOR_DEV_CLASS_MOISTURE_DEF, 22 | CONF_BINARY_SENSOR_DEV_CLASS_OPENING_DEF, 23 | ) 24 | 25 | _LOGGER = logging.getLogger(__name__) 26 | 27 | 28 | class SauresSensor(Entity): 29 | """Representation of a Sensor.""" 30 | 31 | _state: str 32 | 33 | def __init__( 34 | self, 35 | hass, 36 | controller, 37 | flat_id, 38 | meter_id, 39 | sn, 40 | counter_name, 41 | is_debug, 42 | scan_interval, 43 | ): 44 | """Initialize the sensor.""" 45 | 46 | self.controller: SauresHA = controller 47 | self.flat_id = flat_id 48 | self.serial_number = str(sn) 49 | self.counter_name = counter_name 50 | self.isStart = True 51 | self.isDebug = is_debug 52 | self._attributes = dict() 53 | self._state = "" 54 | self.meter_id = meter_id 55 | self.scan_interval = scan_interval 56 | 57 | self._unique_id = slugify(f"sauresha_{flat_id}_{meter_id}") 58 | 59 | self.set_scan_interval(hass, timedelta(minutes=scan_interval)) 60 | 61 | def set_scan_interval(self, hass: object, scan_interval: timedelta): 62 | """Update scan interval.""" 63 | 64 | async def refresh(event_time): 65 | await self.async_update() 66 | 67 | if self.isDebug: 68 | _LOGGER.warning("Scan_interval = %s", str(scan_interval)) 69 | 70 | async_track_time_interval(hass, refresh, scan_interval) 71 | 72 | @property 73 | def unique_id(self): 74 | """Return a unique ID to use for this sensor.""" 75 | return f"sauresha_{self.flat_id}_{self.meter_id}" 76 | 77 | @property 78 | def current_meter(self): 79 | return self.controller.get_sensor(self.flat_id, self.meter_id) 80 | 81 | @property 82 | def name(self): 83 | if not self.counter_name: 84 | self.counter_name = f" [{self.flat_id}] [{self.meter_id}]" 85 | return f"[SAURES] {self.counter_name}" 86 | 87 | @property 88 | def state(self): 89 | """Return the state of the sensor.""" 90 | return self._state 91 | 92 | @property 93 | def icon(self): 94 | return "mdi:counter" 95 | 96 | @property 97 | def extra_state_attributes(self): 98 | return self._attributes 99 | 100 | async def async_fetch_state(self): 101 | """Retrieve latest state.""" 102 | str_return_value = "Unknown" 103 | 104 | if self.isDebug: 105 | _LOGGER.warning("Update Start meter_id: %s", str(self.meter_id)) 106 | 107 | try: 108 | 109 | lock = asyncio.Lock() 110 | async with lock: 111 | await self.controller.async_fetch_data() 112 | 113 | meter = self.current_meter 114 | str_return_value = meter.value 115 | if meter.type_number == 8: 116 | self._attributes.update( 117 | { 118 | "friendly_name": meter.name, 119 | "condition": meter.state, 120 | "sn": meter.sn, 121 | "type": meter.type, 122 | "meter_id": meter.meter_id, 123 | "input": meter.input, 124 | "approve_dt": meter.approve_dt, 125 | "t1": meter.t1, 126 | "t2": meter.t2, 127 | "t3": meter.t3, 128 | "t4": meter.t4, 129 | } 130 | ) 131 | else: 132 | self._attributes.update( 133 | { 134 | "friendly_name": meter.name, 135 | "condition": meter.state, 136 | "sn": meter.sn, 137 | "type": meter.type, 138 | "meter_id": meter.meter_id, 139 | "input": meter.input, 140 | "approve_dt": meter.approve_dt, 141 | } 142 | ) 143 | if self.isStart: 144 | if meter.type_number == 1 or meter.type_number == 2: 145 | self._attributes.update( 146 | {"unit_of_measurement": "m³", 147 | "device_class": "water", 148 | "state_class": "total_increasing"} 149 | ) 150 | elif meter.type_number == 3: 151 | self._attributes.update( 152 | { 153 | "unit_of_measurement": "m³", 154 | "device_class": "gas", 155 | "state_class": "total_increasing", 156 | } 157 | ) 158 | elif meter.type_number == 5: 159 | self._attributes.update({"unit_of_measurement": "°C"}) 160 | elif meter.type_number == 8: 161 | self._attributes.update( 162 | { 163 | "unit_of_measurement": "kWh", 164 | "device_class": "energy", 165 | "state_class": "total_increasing", 166 | } 167 | ) 168 | 169 | self.isStart = False 170 | 171 | self._attributes.update({"last_update_time": datetime.datetime.now()}) 172 | 173 | self._attributes.update( 174 | { 175 | "next_update_time": datetime.datetime.now() 176 | + timedelta(minutes=self.scan_interval) 177 | } 178 | ) 179 | if self.isDebug: 180 | _LOGGER.warning("Update Finish meter_id: %s", str(self.meter_id)) 181 | 182 | except Exception as err: 183 | _LOGGER.error(err) 184 | 185 | return str_return_value 186 | 187 | async def async_update(self): 188 | self._state = await self.async_fetch_state() 189 | 190 | 191 | class SauresBinarySensor(Entity): 192 | """Representation of a BinarySensor.""" 193 | 194 | def __init__( 195 | self, 196 | hass, 197 | controller, 198 | flat_id, 199 | object_type, 200 | meter_id, 201 | serial_number, 202 | counter_name, 203 | is_debug, 204 | scan_interval, 205 | ): 206 | """Initialize the sensor.""" 207 | 208 | self.controller: SauresHA = controller 209 | self.flat_id = flat_id 210 | self.object_type = object_type 211 | self.meter_id = meter_id 212 | self.serial_number = serial_number 213 | self.counter_name = counter_name 214 | self._attributes = dict() 215 | self.isDebug = is_debug 216 | self._state = False 217 | self.isStart = True 218 | self.scan_interval = scan_interval 219 | 220 | self._unique_id = slugify(f"sauresha_{flat_id}_{meter_id}") 221 | 222 | self.set_scan_interval(hass, timedelta(minutes=self.scan_interval)) 223 | 224 | def set_scan_interval(self, hass: object, scan_interval: timedelta): 225 | """Update scan interval.""" 226 | 227 | async def refresh(event_time): 228 | await self.async_update() 229 | 230 | if self.isDebug: 231 | _LOGGER.warning("Scan_interval = %s", str(scan_interval)) 232 | 233 | async_track_time_interval(hass, refresh, scan_interval) 234 | 235 | @property 236 | def current_sensor(self): 237 | return self.controller.get_binarysensor(self.flat_id, self.meter_id) 238 | 239 | @property 240 | def unique_id(self): 241 | """Return a unique ID to use for this sensor.""" 242 | return f"sauresha_{self.flat_id}_{self.meter_id}" 243 | 244 | @property 245 | def current_meter(self): 246 | return self.controller.get_sensor(self.flat_id, self.meter_id) 247 | 248 | @property 249 | def name(self): 250 | """Return the entity_id of the sensor.""" 251 | if not self.counter_name: 252 | self.counter_name = f" [{self.flat_id}] [{self.meter_id}]" 253 | return f"[SAURES] {self.counter_name}" 254 | 255 | @property 256 | def is_on(self): 257 | """Return true if the binary sensor is on.""" 258 | return bool(int(self._state)) 259 | 260 | @property 261 | def state(self): 262 | """Return the state of the sensor.""" 263 | return bool(int(self._state)) 264 | 265 | @property 266 | def available(self): 267 | """Return true if the binary sensor is available.""" 268 | return self._state is not None 269 | 270 | @property 271 | def device_class(self): 272 | if self.object_type in CONF_BINARY_SENSOR_DEV_CLASS_MOISTURE_DEF: 273 | return BinarySensorDeviceClass.MOISTURE 274 | elif self.object_type in CONF_BINARY_SENSOR_DEV_CLASS_OPENING_DEF: 275 | return BinarySensorDeviceClass.OPENING 276 | else: 277 | return "None" 278 | 279 | @property 280 | def extra_state_attributes(self): 281 | return self._attributes 282 | 283 | async def async_fetch_state(self): 284 | """Retrieve latest state.""" 285 | if self.isDebug: 286 | _LOGGER.warning("Update Start meter_id: %s", str(self.meter_id)) 287 | 288 | lock = asyncio.Lock() 289 | async with lock: 290 | await self.controller.async_fetch_data() 291 | 292 | meter = self.current_sensor 293 | return_value = meter.value 294 | self._attributes.update( 295 | { 296 | "friendly_name": meter.name, 297 | "condition": meter.state, 298 | "sn": meter.sn, 299 | "type": meter.type, 300 | "meter_id": meter.meter_id, 301 | "input": meter.input, 302 | } 303 | ) 304 | if meter.state is not None: 305 | if meter.state.upper() == "ОБРЫВ": 306 | return_value = True 307 | else: 308 | _LOGGER.error("API ERROR during Auth process") 309 | 310 | if self.isStart: 311 | if meter.type_number == 10: 312 | self._attributes.update( 313 | { 314 | "device_class": "opening", 315 | } 316 | ) 317 | self.isStart = False 318 | 319 | self._attributes.update({"last_update_time": datetime.datetime.now()}) 320 | 321 | self._attributes.update( 322 | { 323 | "next_update_time": datetime.datetime.now() 324 | + timedelta(minutes=self.scan_interval) 325 | } 326 | ) 327 | if self.isDebug: 328 | _LOGGER.warning("Update Finish meter_id: %s", str(self.meter_id)) 329 | 330 | return return_value 331 | 332 | async def async_update(self): 333 | self._state = await self.async_fetch_state() 334 | 335 | 336 | class SauresControllerSensor(Entity): 337 | """Representation of a Sensor.""" 338 | 339 | _state: str 340 | 341 | def __init__( 342 | self, 343 | hass, 344 | controller, 345 | flat_id, 346 | sn, 347 | counter_name, 348 | is_debug, 349 | scan_interval, 350 | ): 351 | """Initialize the sensor.""" 352 | self.controller: SauresHA = controller 353 | self.flat_id = flat_id 354 | self.serial_number = str(sn) 355 | self.counter_name = str(counter_name) 356 | self._state = "" 357 | self.isDebug = is_debug 358 | self._attributes = dict() 359 | self.scan_interval = scan_interval 360 | 361 | self._unique_id = slugify(f"sauresha_{flat_id}_{sn}") 362 | 363 | self.set_scan_interval(hass, timedelta(minutes=scan_interval)) 364 | 365 | def set_scan_interval(self, hass: object, scan_interval: timedelta): 366 | """Update scan interval.""" 367 | 368 | async def refresh(event_time): 369 | await self.async_update() 370 | 371 | if self.isDebug: 372 | _LOGGER.warning("Scan_interval = %s", str(scan_interval)) 373 | 374 | async_track_time_interval(hass, refresh, scan_interval) 375 | 376 | @property 377 | def current_controller_info(self): 378 | return self.controller.get_controller(self.flat_id, self.serial_number) 379 | 380 | @property 381 | def device_info(self) -> DeviceInfo: 382 | """Return a description for device registry.""" 383 | my_controller = self.current_controller_info 384 | info = DeviceInfo( 385 | identifiers={ 386 | # Serial numbers are unique identifiers within a specific domain 387 | (DOMAIN, self.unique_id) 388 | }, 389 | name=self.name, 390 | manufacturer="SAURES", 391 | model=self.controller.get_controller_name(my_controller.hardware), 392 | sw_version=my_controller.firmware, 393 | via_device=( 394 | DOMAIN, 395 | f"[{self.controller.get_controller_name(my_controller.hardware)}]:[{self.serial_number}]", 396 | ), 397 | ) 398 | 399 | return info 400 | 401 | @property 402 | def unique_id(self): 403 | """Return the entity_id of the sensor.""" 404 | return f"sauresha_contr_{self.flat_id}_{self.serial_number}" 405 | 406 | @property 407 | def name(self): 408 | """Return the entity_id of the sensor.""" 409 | if not self.counter_name: 410 | self.counter_name = f" [{self.flat_id}] [{self.serial_number}]" 411 | return f"[SAURES] {self.counter_name}" 412 | 413 | @property 414 | def state(self): 415 | """Return the state of the sensor.""" 416 | return self._state 417 | 418 | @property 419 | def icon(self): 420 | return "mdi:home-circle" 421 | 422 | @property 423 | def extra_state_attributes(self): 424 | return self._attributes 425 | 426 | async def async_fetch_state(self): 427 | """Retrieve latest state.""" 428 | str_return_value = "Unknown" 429 | if self.isDebug: 430 | _LOGGER.warning("Update Start sn: %s", str(self.serial_number)) 431 | 432 | lock = asyncio.Lock() 433 | async with lock: 434 | await self.controller.async_fetch_data() 435 | 436 | my_controller = self.current_controller_info 437 | str_return_value = my_controller.state 438 | self._attributes.update( 439 | { 440 | "friendly_name": self.counter_name, 441 | ATTR_BATTERY_LEVEL: my_controller.battery, 442 | "condition": my_controller.state, 443 | "sn": my_controller.sn, 444 | "local_ip": my_controller.local_ip, 445 | "firmware": my_controller.firmware, 446 | "ssid": my_controller.ssid, 447 | "readout_dt": my_controller.readout_dt, 448 | "request_dt": my_controller.request_dt, 449 | "rssi": my_controller.rssi, 450 | "hardware": my_controller.hardware, 451 | "hardware_name": self.controller.get_controller_name( 452 | my_controller.hardware 453 | ), 454 | "new_firmware": my_controller.new_firmware, 455 | "last_connection": my_controller.last_connection, 456 | "last_connection_warning": my_controller.last_connection_warning, 457 | "check_hours": my_controller.check_hours, 458 | "check_period_display": my_controller.check_period_display, 459 | "requests": my_controller.requests, 460 | "log": my_controller.log, 461 | "cap_state": my_controller.cap_state, 462 | "power_supply": my_controller.power_supply, 463 | } 464 | ) 465 | self._attributes.update({"last_update_time": datetime.datetime.now()}) 466 | 467 | self._attributes.update( 468 | { 469 | "next_update_time": datetime.datetime.now() 470 | + timedelta(minutes=self.scan_interval) 471 | } 472 | ) 473 | if self.isDebug: 474 | _LOGGER.warning("Update Finish sn: %s", str(self.serial_number)) 475 | return str_return_value 476 | 477 | async def async_update(self): 478 | self._state = await self.async_fetch_state() 479 | 480 | 481 | class SauresSwitch(SwitchEntity): 482 | """Representation of a Switch.""" 483 | 484 | _state: str 485 | 486 | def __init__( 487 | self, 488 | hass, 489 | controller, 490 | flat_id, 491 | meter_id, 492 | sn, 493 | counter_name, 494 | is_debug, 495 | scan_interval, 496 | ): 497 | """Initialize the switch.""" 498 | 499 | self.controller: SauresHA = controller 500 | self.flat_id = flat_id 501 | self.serial_number = str(sn) 502 | self.counter_name = counter_name 503 | self.isStart = True 504 | self.isDebug = is_debug 505 | self._attributes = dict() 506 | self._state = "" 507 | self.meter_id = meter_id 508 | self.scan_interval = scan_interval 509 | 510 | self._unique_id = slugify(f"sauresha_{flat_id}_{meter_id}") 511 | self.set_scan_interval(hass, timedelta(minutes=scan_interval)) 512 | 513 | def set_scan_interval(self, hass: object, scan_interval: timedelta): 514 | """Update scan interval.""" 515 | 516 | async def refresh(event_time): 517 | await self.async_update() 518 | 519 | if self.isDebug: 520 | _LOGGER.warning("Scan_interval = %s", str(scan_interval)) 521 | 522 | async_track_time_interval(hass, refresh, scan_interval) 523 | 524 | @property 525 | def current_meter(self): 526 | return self.controller.get_switch(self.flat_id, self.meter_id) 527 | 528 | @property 529 | def unique_id(self): 530 | """Return a unique ID to use for this sensor.""" 531 | return f"sauresha_switch_{self.flat_id}_{self.serial_number}" 532 | 533 | @property 534 | def name(self): 535 | """Return the entity_id of the sensor.""" 536 | if not self.counter_name: 537 | self.counter_name = f"[SAURES][{self.flat_id}] [{self.meter_id}]" 538 | return f"[SAURES] {self.counter_name}" 539 | 540 | async def async_turn_on(self, **kwargs) -> None: 541 | """Turn the entity on.""" 542 | result = await self.controller.set_command(self.meter_id, CONF_COMMAND_ACTIVATE) 543 | if result: 544 | await self.controller.async_get_switches(self.flat_id, True) 545 | 546 | async def async_turn_off(self, **kwargs) -> None: 547 | """Turn the entity off.""" 548 | result = await self.controller.set_command( 549 | self.meter_id, CONF_COMMAND_DEACTIVATE 550 | ) 551 | if result: 552 | await self.controller.async_get_switches(self.flat_id, True) 553 | 554 | @property 555 | def is_on(self): 556 | """Return true if the binary sensor is on.""" 557 | cur_StringValue = str(self._state) 558 | if cur_StringValue.isnumeric(): 559 | return bool(int(self._state)) 560 | else: 561 | return False 562 | 563 | @property 564 | def icon(self): 565 | return "mdi:switch" 566 | 567 | @property 568 | def extra_state_attributes(self): 569 | return self._attributes 570 | 571 | async def async_fetch_state(self): 572 | """Retrieve latest state.""" 573 | str_return_value = "Unknown" 574 | 575 | if self.isDebug: 576 | _LOGGER.warning("Update Start meter_id: %s", str(self.meter_id)) 577 | 578 | lock = asyncio.Lock() 579 | async with lock: 580 | await self.controller.async_fetch_data() 581 | 582 | meter = self.current_meter 583 | str_return_value = meter.value 584 | if meter.type_number == 8: 585 | self._attributes.update( 586 | { 587 | "friendly_name": meter.name, 588 | "condition": meter.state, 589 | "sn": meter.sn, 590 | "type": meter.type, 591 | "meter_id": meter.meter_id, 592 | "input": meter.input, 593 | "approve_dt": meter.approve_dt, 594 | "t1": meter.t1, 595 | "t2": meter.t2, 596 | "t3": meter.t3, 597 | "t4": meter.t4, 598 | } 599 | ) 600 | else: 601 | self._attributes.update( 602 | { 603 | "friendly_name": meter.name, 604 | "condition": meter.state, 605 | "sn": meter.sn, 606 | "type": meter.type, 607 | "meter_id": meter.meter_id, 608 | "input": meter.input, 609 | "approve_dt": meter.approve_dt, 610 | } 611 | ) 612 | self.isStart = False 613 | 614 | self._attributes.update({"last_update_time": datetime.datetime.now()}) 615 | 616 | self._attributes.update( 617 | { 618 | "next_update_time": datetime.datetime.now() 619 | + timedelta(minutes=self.scan_interval) 620 | } 621 | ) 622 | if self.isDebug: 623 | _LOGGER.warning("Update Finish meter_id: %s", str(self.meter_id)) 624 | return str_return_value 625 | 626 | async def async_update(self): 627 | self._state = await self.async_fetch_state() 628 | --------------------------------------------------------------------------------