From e9978cdbd2ae316f780b3d06362c0b9c1e415788 Mon Sep 17 00:00:00 2001 From: Alexander Ryazanov Date: Tue, 17 May 2022 23:56:38 +0300 Subject: [PATCH] refactor: updates for Home Assistant v2022.05.0+ fix: fixed certain sensor features not working due to inheritance refactor: substituted selectors config to target integration chore: cleaned up obsolete code BREAKING CHANGE: removed dev_presentation key completely BREAKING CHANGE: reset device classes --- README.md | 1614 +++++++------- custom_components/lkcomu_interrao/_base.py | 102 +- custom_components/lkcomu_interrao/_schema.py | 3 - .../lkcomu_interrao/binary_sensor.py | 55 +- .../lkcomu_interrao/config_flow.py | 288 +-- custom_components/lkcomu_interrao/const.py | 1 - custom_components/lkcomu_interrao/sensor.py | 1945 ++++++++--------- .../lkcomu_interrao/services.yaml | 15 +- hacs.json | 2 +- requirements.txt | 7 - 10 files changed, 1762 insertions(+), 2270 deletions(-) delete mode 100644 requirements.txt diff --git a/README.md b/README.md index 17a503b..330dd82 100644 --- a/README.md +++ b/README.md @@ -1,807 +1,807 @@ -_ЕЛК ЖКХ «Интер РАО»_ для _Home Assistant_ -================================================== -Логотип интеграции - -> Предоставление информации о текущем состоянии ваших аккаунтов в ЕЛК ЖКХ. -> -> Information retrieval from Inter RAO personal cabinets. -> ->[![hacs_badge](https://img.shields.io/badge/HACS-Default-green.svg)](https://github.com/custom-components/hacs) -> [![Лицензия](https://img.shields.io/badge/%D0%9B%D0%B8%D1%86%D0%B5%D0%BD%D0%B7%D0%B8%D1%8F-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -> [![Поддержка](https://img.shields.io/badge/%D0%9F%D0%BE%D0%B4%D0%B4%D0%B5%D1%80%D0%B6%D0%B8%D0%B2%D0%B0%D0%B5%D1%82%D1%81%D1%8F%3F-%D0%B4%D0%B0-green.svg)](https://github.com/alryaz/hass-lkcomu-interrao/graphs/commit-activity) -> ->[![Пожертвование Yandex](https://img.shields.io/badge/%D0%9F%D0%BE%D0%B6%D0%B5%D1%80%D1%82%D0%B2%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-Yandex-red.svg)](https://money.yandex.ru/to/410012369233217) -> [![Пожертвование PayPal](https://img.shields.io/badge/%D0%9F%D0%BE%D0%B6%D0%B5%D1%80%D1%82%D0%B2%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-Paypal-blueviolet.svg)](https://www.paypal.me/alryaz) - -## Установка - -### Посредством HACS - -> **✔️️ Рекомендуемый метод** - -1. Установите - HACS ([инструкция по установке на оф. сайте](https://hacs.xyz/docs/setup/prerequisites/)) -2. Найдите `lkcomu_interrao` (или `Интер РАО`) в поиске по интеграциям 1 -3. Установите последнюю версию компонента, нажав на кнопку `Установить` (`Install`) -4. Перезапустите Home Assistant - -### Вручную - -> **⚠️ Не рекомендуется** - -1. Скачайте архив с исходным кодом дополнения -2. Извлеките папку `lkcomu_interrao` из архива в папку `custom_components` внутри папки с - конфигурацией Home Assistant (создайте её, если она отсутствует) -3. Перезапустите Home Assistant - -## Настройка - -### Через раздел интеграции -1. Перейдите в подраздел _"[Интеграции](https://my.home-assistant.io/redirect/integrations)"_ в разделе _"Настройки"_ -2. Нажмите кнопку _"Добавить интеграцию"_ -3. Введите в поисковую строку: **_Личный кабинет Интер РАО (Энергосбыт)_** (англ. **_Inter RAO Personal Cabinet (Energosbyt)_**) -4. Выберите найденную интеграцию -5. Следуйте инструкциям мастера по добавлению - -_Примечание:_ Поле **_Заголовок User-Agent_** (англ. **_User-Agent header_**) генерируется автоматически. - -### Описание конфигурационной схемы -```yaml -# Файл `configuration.yaml` -lkcomu_interrao: - - # Тип выбранного ЛК - # Значение по умолчанию: moscow - # Перечень возможных значений: - # - altai (ЛК Алтай (АО «АлтайЭнергосбыт»)) - # - bashkortostan (ЛКК ЭСКБ (Башэлектросбыт)) - # - moscow (ЕЛК ЖКХ (АО «Мосэнергосбыт», МосОблЕИРЦ, ПАО «Россети Московский регион»)) - # - oryol (ЛКК Орел (ООО «Орловский энергосбыт»)) - # - saratov (ЛК Саратов (ПАО «Саратовэнерго»)) - # - sevesk (ЕЛК Вологда (Северная сбытовая компания)) - # - tambov (ЛК ТЭСК (Тамбовская энергосбытовая компания)) - # - tomsk (ЕЛК Томск (Томскэнергосбыт / Томск РТС)) - # - volga (ЛКК ЭСВ (Энергосбыт Волга)) - type: "..." - - # Имя пользователя - # Обязательный параметр - username: "..." - - # Пароль - # Обязательный параметр - password: "..." - - # Конфигурация по умолчанию для лицевых счетов - # Необязательный параметр - # # Данная конфигурация применяется, если отсутствует # конкретизация, указанная в разделе `accounts`. - default: - - # Получать ли ссылки на логотипы - # Значение по умолчанию: истина (true) - logos: true | false - - # Добавлять ли объект(-ы): Информация о лицевом счёте - # Значение по умолчанию: истина (true) - accounts: true | false - - # Добавлять ли объект(-ы): Счётчик коммунальных услуг - # Значение по умолчанию: истина (true) - meters: true | false - - # Добавлять ли объект(-ы): Последний зарегистрированный платёж - # Значение по умолчанию: истина (true) - last_payment: true | false - - # Добавлять ли объект(-ы): Последняя выпущенная квитанция - # Значение по умолчанию: истина (true) - last_invoice: true | false - - # Настройки для отдельных лицевых счетов - # Необязательный параметр - accounts: - - # Номер лицевого счёта - "...": - - # Конфигурация по конкретным лицевым счетам выполняется аналогично - # конфигурации по умолчанию для лицевых счетов (раздел `default`). - ... -``` - -### Вариант конфигурации "Чёрный список" - -Для реализации белого списка, конфигурация выполняется следующим образом: -```yaml -... -lkcomu_interrao: - ... - # Выборочное исключение лицевых счетов - accounts: - # Все указанные ниже лицевые счета будут добавлены - "12345-678-90": false - "98765-432-10": false - "111000111000": false -``` - -### Вариант конфигурации "Белый список" - -Для реализации белого списка, конфигурация выполняется следующим образом: -```yaml -... -lkcomu_interrao: - ... - # Отключение добавление лицевых счетов по умолчанию - default: false - - # Выборочное включение лицевых сченов - accounts: - # Все указанные ниже лицевые счета будут добавлены - "12345-678-90": true - "98765-432-10": true - "111000111000": true -``` - -Также возможно использовать укороченную запись: -```yaml -... -lkcomu_interrao: - ... - # Данный пример функционально эквивалентен предыдущему примеру - default: false - accounts: ["12345-678-90", "98765-432-10", "111000111000"] -``` - -## Доступные объекты - -Все объекты гарантируют наличие и полноту следующих атрибутов: - -- `account_code: str` - Номер лицевого счёта -- `account_id: int` - Внутренний идентификатор лицевого счёта - -### Лицевые счета — `lkcomu_interrao_account` - -> **Домен объектов:** `sensor` - -Объект лицевого счёта отображает основную информацию о лицевом счёте, а также его баланс -(положительное значение) или имеющуюся задолженность (отрицательное значение) 1. - -Состояние объекта может принимать следующие значения: - -- `unknown` - Информация о состоянии баланса не была предоставлена -- _число_ - Текущее состояние баланса - -_1 ... в том случае, если лицевой счёт предоставляет информацию о балансе_ - -#### Сопутствующие службы - -> Данные службы применимы только к объектам лицевых счетов - -##### `set_description` — Установить описание лицевого счёта - -Устанавливает описание для лицевого счёта и провоцирует его обновление. - -###### Параметры - -- `description: str | None` - _(опционально)_ Новое описание для лицевого счёта - -###### Результат - -Событие с идентификатором `lkcomu_interrao_set_description` и следующими значениями: -- `success: bool` - Если установка описания была выполнена успешно -- `description: str | None` - Описание, с которым была вызвана служба -- `previous: str | None` - Описание, которым обладал (или, в случае ошибки, обладает) лицевой счёт -- `account_id: int` - Внутренний идентификатор лицевого счёта -- `account_code: str` - Номер лицевого счёта - -##### `get_invoices` — Получение квитанций по периодам - -> Только для объектов, поддерживающих данный функционал - -###### Параметры - -- `start: str | None` - _(опционально)_ Дата начала периода -- `end: str | None` - _(опционально)_ Дата окончания периода - -###### Результат - -Событие с идентификатором `lkcomu_interrao_get_invoices` и следующими значениями: - -- `sum: float` - сумма всех квитанций за указанный период -- `period: str` - период квитанции -- `invoice_id: str` - идентификатор квитанции -- `total: float` - сумма к оплате по квитанции -- `paid: float | None` - сумма оплат, учтённых к квитанции -- `initial: float | None` - задолженность/избыток на начало периода -- `charged: float | None` - начислено за период -- `insurance: float | None` - добровольное страхование -- `benefits: float | None` - льготы -- `penalty: float | None` - штрафы -- `service: float | None` - тех. обслуживание - -##### `get_payments` — Получение платежей по периодам - -> Только для объектов, поддерживающих данный функционал - -###### Параметры - -- `start: str | None` - _(опционально)_ Дата начала периода -- `end: str | None` - _(опционально)_ Дата окончания периода - -###### Результат - -Событие с идентификатором `lkcomu_interrao_get_payments` и следующими значениями: - -- `sum: float` - сумма всех платежей за указанный период -- `amount: float` - объём платежа -- `paid_at: str` - дата/время платежа -- `period: str` - период, за который платёж был выполнен -- `status: str | None` - состояние платежа -- `agent: str | None` - банк-обработчик платежа -- `group: str | None` - группа платежа (для лицевых счетов с несколькими типами платежей) - -
- -### Счётчики — `lkcomu_interrao_meter` - -> **Домен объектов:** `sensor` - -Объект счётчика отображает информацию о счётчике, а также сведения о последних переданных показаниях -и диапазоне периода передачи показаний1. - -Состояние объекта может принимать следующие значения: - -- `ok` - Текстовое описание состояния отсутствует -- _текст_ - Текстовое описание состояние счётчика (может быть любой длины, и содержать в себе любой - набор символов, в т.ч. HTML-теги) - -Объект гарантирует наличие и полноту следующих атрибутов: - -- `meter_code` - Номер счётчика -- `install_date` - Дата установки -- `submit_period_start` - Дата начала периода передачи показаний (в текущем месяце) 1 -- `submit_period_end` - Дата окончания периода передачи показаний (в текущем месяце) 1 -- `submit_period_active` - Флаг активности периода передачи показаний 1 -- `zone_t[N]_name` - Наименование тарифной зоны / тарифа -- `zone_t[N]_last_indication` - Последнее показание по тарифной зоне 2 - -Объект гарантирует наличие, но не полноту следующих атрибутов: - -- `model` - Модель счётчика -- `last_indications_date` - Дата последней передачи показаний -- `zone_t[N]_description` - Описание тарифной зоны / тарифа -- `zone_t[N]_today_indication` - Значение переданного сегодня показания по тарифной зоне -- `zone_t[N]_invoice_indication` - Значение последнего показания по тарифной зоне, учтённому в - квитанции -- `zone_t[N]_period_indication` - Значение переданного за период показания по тарифной зоне - 1 -- `zone_t[N]_invoice_name` - Наименование тарифной зоны, указанное в последней квитанции - -_1 ... в том случае, если счётчик поддерживает передачу показаний_
-_2 При отсутствии фактического значения атрибут примет значение `0.0`_ - -#### Сопутствующие службы - -> Данные службы применимы только к объектам счётчиков - -##### `push_indications` — Передача показаний - -> Только для объектов, поддерживающих данный функционал - -Служба передачи показаний позволяет отправлять показания по счётчикам в личный кабинет, и -имеет следующий набор параметров: - -| Название | Описание | -| --- | --- | -| `target` | Выборка целевых объектов, для которых требуется передавать показания | -| `data`.`indications` | Список / именованный массив показаний, передаваемых в ЛК | -| `data`.`incremental` | Суммирование текущих показаний с передаваемыми | -| `data`.`ignore_period` | Игнорировать период передачи показаний | -| `data`.`ignore_indications` | Игнорировать ограничения по значениям | - -###### 1. Обычная передача показаний - -- Например, если передача показаний активна с 15 по 25 число, а сегодня 11, то показания - **не будут** отправлены1. -- Например, если текущие, последние или принятые значения по счётчику – 321, 654 и 987 по зонам - _Т1_, _Т2_ и _Т3_ соответственно, то показания **не будут** - отправлены1. - -```yaml -service: lkcomu_interrao.push_indications -data: - indications: "123, 456, 789" -target: - entity_id: sensor.1243145122_meter_123456789 -``` - -... или, с помощью именованного массива: - -```yaml -service: lkcomu_interrao.calculate_indications -data: - indications: - t1: 123 - t2: 456 - t3: 789 -target: - entity_id: sensor.1243145122_meter_123456789 -``` - -... или, с помощью списка: - -```yaml -service: lkcomu_interrao.calculate_indications -data: - indications: [123, 456, 789] -target: - entity_id: sensor.1243145122_meter_123456789 -``` - -###### 2. Форсированная передача показаний - -Отключение всех ограничений по показаниям. - -- Например, если передача показаний активна с 15 по 25 число, а сегодня 11, то показания - **будут** отправлены1. -- Например, если текущие, последние или принятые значения по счётчику – 321, 654 и 987 по зонам - _Т1_, _Т2_ и _Т3_ соответственно, то показания **будут** - отправлены1. - -```yaml -service: lkcomu_interrao.calculate_indications -data_template: - indications: [123, 456, 789] - ignore_indications: true - ignore_periods: true -target: - entity_id: sensor.1243145122_meter_123456789 -``` - -###### 3. Сложение показаний - -- Например, если передача показаний активна с 15 по 25 число, а сегодня 11, то показания - **не будут** отправлены1. -- Например, если текущие, последние или принятые значения по счётчику – 321, 654 и 987 по зонам - _Т1_, _Т2_ и _Т3_ соответственно, то показания **будут** - отправлены1. - -**Внимание:** в данном примере будут отправлены показания _444_, _1110_ и _1776_, -а не _123_, _456_ и _789_. - -```yaml -service: lkcomu_interrao.calculate_indications -data_template: - indications: [123, 456, 789] - incremental: true -target: - entity_id: sensor.1243145122_meter_123456789 -``` - -##### `calculate_indications` — Подсчёт показаний - -> Только для объектов, поддерживающих данный функционал - -
- -### Последние платежи — `lkcomu_interrao_last_payment` - -> **Домен объектов:** `binary_sensor` - -Объект последнего платежа отображает информацию о последнем зарегистрированном платеже, связанном с -лицевым счётом. - -Состояние объекта может принимать следующие значения: - -- `on` - Платёж был обработан -- `off` - Платёж ещё не обработан -- `unknown` - Последний платёж не был найден - -Объект гарантирует наличие и полноту следующих атрибутов: - -- `amount: float` - Сумма платежа -- `paid_at: str` - Дата и время платежа -- `period: str` - Период, за который был выполнен платёж - -Объект гарантирует наличие, но не полноту, следующих атрибутов: - -- `status: str | None` - Состояние платежа -- `agent: str | None` - Банк, проводящий платёж -- `group: str | None` - Группа платежа (для лицевых счетов с несколькими источниками платежей) - -
- -### Последние квитанции — `lkcomu_interrao_last_invoice` - -> **Домен объектов:** `sensor` - -> @ TODO @ - -
- -## Поддерживаемые ЛК - -Ниже предъявлен перечень поддерживаемых ЛК с их внутренними идентификаторами. -Данные идентификаторы используются как значение для поля `type`. - -> **Внимание:** Поддерживаются только ЛК физических лиц. Поддержка ЛК юридических лиц не планируется. -### ЕЛК ЖКХ (АО «Мосэнергосбыт», МосОблЕИРЦ, ПАО «Россети Московский регион») - `moscow` -[![Ссылка на личный кабинет "ЕЛК ЖКХ (АО «Мосэнергосбыт», МосОблЕИРЦ, ПАО «Россети Московский регион»)"](https://raw.githubusercontent.com/alryaz/hass-lkcomu-interrao/main/images/headers/moscow.png)](https://my.mosenergosbyt.ru) - -#### Пример конфигурации: - -```yaml -... -lkcomu_interrao: - type: moscow - username: username1 - password: password1 -``` - -#### Поставщик `MES` — Электричество -Для поставщика реализована поддержка следующих объектов: -
- Информация о лицевом счёте - Скриншот -
-
- Последний зарегистрированный платёж - Скриншот -
-
- Последняя выпущенная квитанция - Скриншот -
-
- Счётчик коммунальных услуг - Скриншот -
- -#### Поставщик `KSG` — Электричество -Для поставщика реализована поддержка следующих объектов: -
- Информация о лицевом счёте - Скриншот -
-
- Последний зарегистрированный платёж - Скриншот -
-
- Последняя выпущенная квитанция - Скриншот -
-
- Счётчик коммунальных услуг - Скриншот -
- -#### Поставщик `MOE` — ЕПД -Для поставщика реализована поддержка следующих объектов: -
- Информация о лицевом счёте - Скриншот -
-
- Последний зарегистрированный платёж - Скриншот -
-
- Последняя выпущенная квитанция - Скриншот -
-
- Счётчик коммунальных услуг - Скриншот -
- -#### Поставщик `TKO` — ТКО -Для поставщика реализована поддержка следующих объектов: -
- Информация о лицевом счёте - Скриншот -
-
- Последний зарегистрированный платёж - Снимок экрана отсутствует -
-
- Последняя выпущенная квитанция - Скриншот -
-
- Счётчик коммунальных услуг - Снимок экрана отсутствует -
- -### ЛКК Орел (ООО «Орловский энергосбыт») - `oryol` -[![Ссылка на личный кабинет "ЛКК Орел (ООО «Орловский энергосбыт»)"](https://raw.githubusercontent.com/alryaz/hass-lkcomu-interrao/main/images/headers/oryol.png)](https://my.interrao-orel.ru) - -#### Пример конфигурации: - -```yaml -... -lkcomu_interrao: - type: oryol - username: username1 - password: password1 -``` - -#### Поставщик `ORL_EPD` — ЕПД -Для поставщика реализована поддержка следующих объектов: -
- Информация о лицевом счёте - Скриншот -
-
- Последняя выпущенная квитанция - Скриншот -
- -#### Поставщик `ORL` — Электричество -Для поставщика реализована поддержка следующих объектов: -
- Информация о лицевом счёте - Скриншот -
-
- Последний зарегистрированный платёж - Скриншот -
-
- Последняя выпущенная квитанция - Скриншот -
-
- Счётчик коммунальных услуг - Скриншот -
- -### ЛКК ЭСВ (Энергосбыт Волга) - `volga` -[![Ссылка на личный кабинет "ЛКК ЭСВ (Энергосбыт Волга)"](https://raw.githubusercontent.com/alryaz/hass-lkcomu-interrao/main/images/headers/volga.png)](https://my.esbvolga.ru) - -#### Пример конфигурации: - -```yaml -... -lkcomu_interrao: - type: volga - username: username1 - password: password1 -``` - -#### Поставщик `VLD` — Электричество -Для поставщика реализована поддержка следующих объектов: -
- Информация о лицевом счёте - Скриншот -
-
- Последний зарегистрированный платёж - Скриншот -
-
- Последняя выпущенная квитанция - Скриншот -
-
- Счётчик коммунальных услуг - Скриншот -
- -### ЕЛК Томск (Томскэнергосбыт / Томск РТС) - `tomsk` -[![Ссылка на личный кабинет "ЕЛК Томск (Томскэнергосбыт / Томск РТС)"](https://raw.githubusercontent.com/alryaz/hass-lkcomu-interrao/main/images/headers/tomsk.png)](https://my.tomskenergosbyt.ru) - -#### Пример конфигурации: - -```yaml -... -lkcomu_interrao: - type: tomsk - username: username1 - password: password1 -``` - -#### Поставщик `TMK_NRG` — generic -Для поставщика реализована поддержка следующих объектов: -
- Информация о лицевом счёте - Скриншот -
-
- Последний зарегистрированный платёж - Скриншот -
-
- Последняя выпущенная квитанция - Скриншот -
-
- Счётчик коммунальных услуг - Скриншот -
- -### ЛК ТЭСК (Тамбовская энергосбытовая компания) - `tambov` -[![Ссылка на личный кабинет "ЛК ТЭСК (Тамбовская энергосбытовая компания)"](https://raw.githubusercontent.com/alryaz/hass-lkcomu-interrao/main/images/headers/tambov.png)](https://my.tesk.su) - -#### Пример конфигурации: - -```yaml -... -lkcomu_interrao: - type: tambov - username: username1 - password: password1 -``` - -#### Поставщик `TMB` — Электричество -Для поставщика реализована поддержка следующих объектов: -
- Информация о лицевом счёте - Скриншот -
-
- Последний зарегистрированный платёж - Скриншот -
-
- Последняя выпущенная квитанция - Скриншот -
-
- Счётчик коммунальных услуг - Скриншот -
- -### ЕЛК Вологда (Северная сбытовая компания) - `sevesk` -[![Ссылка на личный кабинет "ЕЛК Вологда (Северная сбытовая компания)"](https://raw.githubusercontent.com/alryaz/hass-lkcomu-interrao/main/images/headers/sevesk.png)](https://lk.sevesk.ru) - -#### Пример конфигурации: - -```yaml -... -lkcomu_interrao: - type: sevesk - username: username1 - password: password1 -``` - -#### Поставщик `VLG` — Электричество -Для поставщика реализована поддержка следующих объектов: -
- Информация о лицевом счёте - Скриншот -
-
- Последний зарегистрированный платёж - Скриншот -
-
- Последняя выпущенная квитанция - Скриншот -
-
- Счётчик коммунальных услуг - Скриншот -
- -### ЛК Саратов (ПАО «Саратовэнерго») - `saratov` -[![Ссылка на личный кабинет "ЛК Саратов (ПАО «Саратовэнерго»)"](https://raw.githubusercontent.com/alryaz/hass-lkcomu-interrao/main/images/headers/saratov.png)](https://my.saratovenergo.ru) - -#### Пример конфигурации: - -```yaml -... -lkcomu_interrao: - type: saratov - username: username1 - password: password1 -``` - -#### Поставщик `SAR` — Электричество -Для поставщика реализована поддержка следующих объектов: -
- Информация о лицевом счёте - Скриншот -
-
- Последний зарегистрированный платёж - Скриншот -
-
- Последняя выпущенная квитанция - Скриншот -
-
- Счётчик коммунальных услуг - Скриншот -
- -### ЛКК ЭСКБ (Башэлектросбыт) - `bashkortostan` -[![Ссылка на личный кабинет "ЛКК ЭСКБ (Башэлектросбыт)"](https://raw.githubusercontent.com/alryaz/hass-lkcomu-interrao/main/images/headers/bashkortostan.png)](https://lkk.bashesk.ru) - -#### Пример конфигурации: - -```yaml -... -lkcomu_interrao: - type: bashkortostan - username: username1 - password: password1 -``` - -#### Поставщик `UFA` — Электричество -Для поставщика реализована поддержка следующих объектов: -
- Информация о лицевом счёте - Скриншот -
-
- Последний зарегистрированный платёж - Скриншот -
-
- Последняя выпущенная квитанция - Скриншот -
-
- Счётчик коммунальных услуг - Скриншот -
- -### ЛК Алтай (АО «АлтайЭнергосбыт») - `altai` -[![Ссылка на личный кабинет "ЛК Алтай (АО «АлтайЭнергосбыт»)"](https://raw.githubusercontent.com/alryaz/hass-lkcomu-interrao/main/images/headers/altai.png)](https://lkfl.altaiensb.com) - -#### Пример конфигурации: - -```yaml -... -lkcomu_interrao: - type: altai - username: username1 - password: password1 -``` - -#### Поставщик `ALT` — Электричество -Для поставщика реализована поддержка следующих объектов: -
- Информация о лицевом счёте - Скриншот -
-
- Последний зарегистрированный платёж - Скриншот -
-
- Последняя выпущенная квитанция - Скриншот -
-
- Счётчик коммунальных услуг - Скриншот -
- -## Дополнительная информация - -Компонент находится в активной разработке. Примерная дорожная карта и план развития: - -- `[0.0.1; 0.1.0)` — первичная обкатка компонента на поддерживаемых поставщиках; -- `[0.1.0; 0.2.0)` — разработка визуального конфигуратора для интеграции; -- `[0.2.0; 1.0.0)` — выполнение задач по оптимизации и чистке кода; -- `[1.0.0; .....)` — финальный выпуск компонента и исправление ошибок. - -Увеличение старшей версии сопутствует: - -- изменениям в конфигурационной схеме; -- удалению функционала; -- расширению сферы применения интеграции. - -Увеличение младшей версии сопутствует: - -- добавлению нового функционала; -- исправлению значительных ошибок. - -Увеличение номера сборки сопутствует: - -- исправлению мелких ошибок / выпуску хотфиксов; -- изменениям во взаимодействии с основополагающей библиотекой (или её версии). +_ЕЛК ЖКХ «Интер РАО»_ для _Home Assistant_ +================================================== +Логотип интеграции + +> Предоставление информации о текущем состоянии ваших аккаунтов в ЕЛК ЖКХ. +> +> Information retrieval from Inter RAO personal cabinets. +> +>[![hacs_badge](https://img.shields.io/badge/HACS-Default-green.svg)](https://github.com/custom-components/hacs) +> [![Лицензия](https://img.shields.io/badge/%D0%9B%D0%B8%D1%86%D0%B5%D0%BD%D0%B7%D0%B8%D1%8F-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +> [![Поддержка](https://img.shields.io/badge/%D0%9F%D0%BE%D0%B4%D0%B4%D0%B5%D1%80%D0%B6%D0%B8%D0%B2%D0%B0%D0%B5%D1%82%D1%81%D1%8F%3F-%D0%B4%D0%B0-green.svg)](https://github.com/alryaz/hass-lkcomu-interrao/graphs/commit-activity) +> +>[![Пожертвование Yandex](https://img.shields.io/badge/%D0%9F%D0%BE%D0%B6%D0%B5%D1%80%D1%82%D0%B2%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-Yandex-red.svg)](https://money.yandex.ru/to/410012369233217) +> [![Пожертвование PayPal](https://img.shields.io/badge/%D0%9F%D0%BE%D0%B6%D0%B5%D1%80%D1%82%D0%B2%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-Paypal-blueviolet.svg)](https://www.paypal.me/alryaz) + +## Установка + +### Посредством HACS + +> **✔️️ Рекомендуемый метод** + +1. Установите + HACS ([инструкция по установке на оф. сайте](https://hacs.xyz/docs/setup/prerequisites/)) +2. Найдите `lkcomu_interrao` (или `Интер РАО`) в поиске по интеграциям 1 +3. Установите последнюю версию компонента, нажав на кнопку `Установить` (`Install`) +4. Перезапустите Home Assistant + +### Вручную + +> **⚠️ Не рекомендуется** + +1. Скачайте архив с исходным кодом дополнения +2. Извлеките папку `lkcomu_interrao` из архива в папку `custom_components` внутри папки с + конфигурацией Home Assistant (создайте её, если она отсутствует) +3. Перезапустите Home Assistant + +## Настройка + +### Через раздел интеграции +1. Перейдите в подраздел _"[Интеграции](https://my.home-assistant.io/redirect/integrations)"_ в разделе _"Настройки"_ +2. Нажмите кнопку _"Добавить интеграцию"_ +3. Введите в поисковую строку: **_Личный кабинет Интер РАО (Энергосбыт)_** (англ. **_Inter RAO Personal Cabinet (Energosbyt)_**) +4. Выберите найденную интеграцию +5. Следуйте инструкциям мастера по добавлению + +_Примечание:_ Поле **_Заголовок User-Agent_** (англ. **_User-Agent header_**) генерируется автоматически. + +### Описание конфигурационной схемы +```yaml +# Файл `configuration.yaml` +lkcomu_interrao: + + # Тип выбранного ЛК + # Значение по умолчанию: moscow + # Перечень возможных значений: + # - altai (ЛК Алтай (АО «АлтайЭнергосбыт»)) + # - bashkortostan (ЛКК ЭСКБ (Башэлектросбыт)) + # - moscow (ЕЛК ЖКХ (АО «Мосэнергосбыт», МосОблЕИРЦ, ПАО «Россети Московский регион»)) + # - oryol (ЛКК Орел (ООО «Орловский энергосбыт»)) + # - saratov (ЛК Саратов (ПАО «Саратовэнерго»)) + # - sevesk (ЕЛК Вологда (Северная сбытовая компания)) + # - tambov (ЛК ТЭСК (Тамбовская энергосбытовая компания)) + # - tomsk (ЕЛК Томск (Томскэнергосбыт / Томск РТС)) + # - volga (ЛКК ЭСВ (Энергосбыт Волга)) + type: "..." + + # Имя пользователя + # Обязательный параметр + username: "..." + + # Пароль + # Обязательный параметр + password: "..." + + # Конфигурация по умолчанию для лицевых счетов + # Необязательный параметр + # # Данная конфигурация применяется, если отсутствует # конкретизация, указанная в разделе `accounts`. + default: + + # Получать ли ссылки на логотипы + # Значение по умолчанию: истина (true) + logos: true | false + + # Добавлять ли объект(-ы): Информация о лицевом счёте + # Значение по умолчанию: истина (true) + accounts: true | false + + # Добавлять ли объект(-ы): Счётчик коммунальных услуг + # Значение по умолчанию: истина (true) + meters: true | false + + # Добавлять ли объект(-ы): Последний зарегистрированный платёж + # Значение по умолчанию: истина (true) + last_payment: true | false + + # Добавлять ли объект(-ы): Последняя выпущенная квитанция + # Значение по умолчанию: истина (true) + last_invoice: true | false + + # Настройки для отдельных лицевых счетов + # Необязательный параметр + accounts: + + # Номер лицевого счёта + "...": + + # Конфигурация по конкретным лицевым счетам выполняется аналогично + # конфигурации по умолчанию для лицевых счетов (раздел `default`). + ... +``` + +### Вариант конфигурации "Чёрный список" + +Для реализации белого списка, конфигурация выполняется следующим образом: +```yaml +... +lkcomu_interrao: + ... + # Выборочное исключение лицевых счетов + accounts: + # Все указанные ниже лицевые счета будут добавлены + "12345-678-90": false + "98765-432-10": false + "111000111000": false +``` + +### Вариант конфигурации "Белый список" + +Для реализации белого списка, конфигурация выполняется следующим образом: +```yaml +... +lkcomu_interrao: + ... + # Отключение добавление лицевых счетов по умолчанию + default: false + + # Выборочное включение лицевых сченов + accounts: + # Все указанные ниже лицевые счета будут добавлены + "12345-678-90": true + "98765-432-10": true + "111000111000": true +``` + +Также возможно использовать укороченную запись: +```yaml +... +lkcomu_interrao: + ... + # Данный пример функционально эквивалентен предыдущему примеру + default: false + accounts: ["12345-678-90", "98765-432-10", "111000111000"] +``` + +## Доступные объекты + +Все объекты гарантируют наличие и полноту следующих атрибутов: + +- `account_code: str` - Номер лицевого счёта +- `account_id: int` - Внутренний идентификатор лицевого счёта + +### Лицевые счета — `lkcomu_interrao_account` + +> **Домен объектов:** `sensor` + +Объект лицевого счёта отображает основную информацию о лицевом счёте, а также его баланс +(положительное значение) или имеющуюся задолженность (отрицательное значение) 1. + +Состояние объекта может принимать следующие значения: + +- `unknown` - Информация о состоянии баланса не была предоставлена +- _число_ - Текущее состояние баланса + +_1 ... в том случае, если лицевой счёт предоставляет информацию о балансе_ + +#### Сопутствующие службы + +> Данные службы применимы только к объектам лицевых счетов + +##### `set_description` — Установить описание лицевого счёта + +Устанавливает описание для лицевого счёта и провоцирует его обновление. + +###### Параметры + +- `description: str | None` - _(опционально)_ Новое описание для лицевого счёта + +###### Результат + +Событие с идентификатором `lkcomu_interrao_set_description` и следующими значениями: +- `success: bool` - Если установка описания была выполнена успешно +- `description: str | None` - Описание, с которым была вызвана служба +- `previous: str | None` - Описание, которым обладал (или, в случае ошибки, обладает) лицевой счёт +- `account_id: int` - Внутренний идентификатор лицевого счёта +- `account_code: str` - Номер лицевого счёта + +##### `get_invoices` — Получение квитанций по периодам + +> Только для объектов, поддерживающих данный функционал + +###### Параметры + +- `start: str | None` - _(опционально)_ Дата начала периода +- `end: str | None` - _(опционально)_ Дата окончания периода + +###### Результат + +Событие с идентификатором `lkcomu_interrao_get_invoices` и следующими значениями: + +- `sum: float` - сумма всех квитанций за указанный период +- `period: str` - период квитанции +- `invoice_id: str` - идентификатор квитанции +- `total: float` - сумма к оплате по квитанции +- `paid: float | None` - сумма оплат, учтённых к квитанции +- `initial: float | None` - задолженность/избыток на начало периода +- `charged: float | None` - начислено за период +- `insurance: float | None` - добровольное страхование +- `benefits: float | None` - льготы +- `penalty: float | None` - штрафы +- `service: float | None` - тех. обслуживание + +##### `get_payments` — Получение платежей по периодам + +> Только для объектов, поддерживающих данный функционал + +###### Параметры + +- `start: str | None` - _(опционально)_ Дата начала периода +- `end: str | None` - _(опционально)_ Дата окончания периода + +###### Результат + +Событие с идентификатором `lkcomu_interrao_get_payments` и следующими значениями: + +- `sum: float` - сумма всех платежей за указанный период +- `amount: float` - объём платежа +- `paid_at: str` - дата/время платежа +- `period: str` - период, за который платёж был выполнен +- `status: str | None` - состояние платежа +- `agent: str | None` - банк-обработчик платежа +- `group: str | None` - группа платежа (для лицевых счетов с несколькими типами платежей) + +
+ +### Счётчики — `lkcomu_interrao_meter` + +> **Домен объектов:** `sensor` + +Объект счётчика отображает информацию о счётчике, а также сведения о последних переданных показаниях +и диапазоне периода передачи показаний1. + +Состояние объекта может принимать следующие значения: + +- `ok` - Текстовое описание состояния отсутствует +- _текст_ - Текстовое описание состояние счётчика (может быть любой длины, и содержать в себе любой + набор символов, в т.ч. HTML-теги) + +Объект гарантирует наличие и полноту следующих атрибутов: + +- `meter_code` - Номер счётчика +- `install_date` - Дата установки +- `submit_period_start` - Дата начала периода передачи показаний (в текущем месяце) 1 +- `submit_period_end` - Дата окончания периода передачи показаний (в текущем месяце) 1 +- `submit_period_active` - Флаг активности периода передачи показаний 1 +- `zone_t[N]_name` - Наименование тарифной зоны / тарифа +- `zone_t[N]_last_indication` - Последнее показание по тарифной зоне 2 + +Объект гарантирует наличие, но не полноту следующих атрибутов: + +- `model` - Модель счётчика +- `last_indications_date` - Дата последней передачи показаний +- `zone_t[N]_description` - Описание тарифной зоны / тарифа +- `zone_t[N]_today_indication` - Значение переданного сегодня показания по тарифной зоне +- `zone_t[N]_invoice_indication` - Значение последнего показания по тарифной зоне, учтённому в + квитанции +- `zone_t[N]_period_indication` - Значение переданного за период показания по тарифной зоне + 1 +- `zone_t[N]_invoice_name` - Наименование тарифной зоны, указанное в последней квитанции + +_1 ... в том случае, если счётчик поддерживает передачу показаний_
+_2 При отсутствии фактического значения атрибут примет значение `0.0`_ + +#### Сопутствующие службы + +> Данные службы применимы только к объектам счётчиков + +##### `push_indications` — Передача показаний + +> Только для объектов, поддерживающих данный функционал + +Служба передачи показаний позволяет отправлять показания по счётчикам в личный кабинет, и +имеет следующий набор параметров: + +| Название | Описание | +| --- | --- | +| `target` | Выборка целевых объектов, для которых требуется передавать показания | +| `data`.`indications` | Список / именованный массив показаний, передаваемых в ЛК | +| `data`.`incremental` | Суммирование текущих показаний с передаваемыми | +| `data`.`ignore_period` | Игнорировать период передачи показаний | +| `data`.`ignore_indications` | Игнорировать ограничения по значениям | + +###### 1. Обычная передача показаний + +- Например, если передача показаний активна с 15 по 25 число, а сегодня 11, то показания + **не будут** отправлены1. +- Например, если текущие, последние или принятые значения по счётчику – 321, 654 и 987 по зонам + _Т1_, _Т2_ и _Т3_ соответственно, то показания **не будут** + отправлены1. + +```yaml +service: lkcomu_interrao.push_indications +data: + indications: "123, 456, 789" +target: + entity_id: sensor.1243145122_meter_123456789 +``` + +... или, с помощью именованного массива: + +```yaml +service: lkcomu_interrao.calculate_indications +data: + indications: + t1: 123 + t2: 456 + t3: 789 +target: + entity_id: sensor.1243145122_meter_123456789 +``` + +... или, с помощью списка: + +```yaml +service: lkcomu_interrao.calculate_indications +data: + indications: [123, 456, 789] +target: + entity_id: sensor.1243145122_meter_123456789 +``` + +###### 2. Форсированная передача показаний + +Отключение всех ограничений по показаниям. + +- Например, если передача показаний активна с 15 по 25 число, а сегодня 11, то показания + **будут** отправлены1. +- Например, если текущие, последние или принятые значения по счётчику – 321, 654 и 987 по зонам + _Т1_, _Т2_ и _Т3_ соответственно, то показания **будут** + отправлены1. + +```yaml +service: lkcomu_interrao.calculate_indications +data_template: + indications: [123, 456, 789] + ignore_indications: true + ignore_periods: true +target: + entity_id: sensor.1243145122_meter_123456789 +``` + +###### 3. Сложение показаний + +- Например, если передача показаний активна с 15 по 25 число, а сегодня 11, то показания + **не будут** отправлены1. +- Например, если текущие, последние или принятые значения по счётчику – 321, 654 и 987 по зонам + _Т1_, _Т2_ и _Т3_ соответственно, то показания **будут** + отправлены1. + +**Внимание:** в данном примере будут отправлены показания _444_, _1110_ и _1776_, +а не _123_, _456_ и _789_. + +```yaml +service: lkcomu_interrao.calculate_indications +data_template: + indications: [123, 456, 789] + incremental: true +target: + entity_id: sensor.1243145122_meter_123456789 +``` + +##### `calculate_indications` — Подсчёт показаний + +> Только для объектов, поддерживающих данный функционал + +
+ +### Последние платежи — `lkcomu_interrao_last_payment` + +> **Домен объектов:** `binary_sensor` + +Объект последнего платежа отображает информацию о последнем зарегистрированном платеже, связанном с +лицевым счётом. + +Состояние объекта может принимать следующие значения: + +- `on` - Платёж был обработан +- `off` - Платёж ещё не обработан +- `unknown` - Последний платёж не был найден + +Объект гарантирует наличие и полноту следующих атрибутов: + +- `amount: float` - Сумма платежа +- `paid_at: str` - Дата и время платежа +- `period: str` - Период, за который был выполнен платёж + +Объект гарантирует наличие, но не полноту, следующих атрибутов: + +- `status: str | None` - Состояние платежа +- `agent: str | None` - Банк, проводящий платёж +- `group: str | None` - Группа платежа (для лицевых счетов с несколькими источниками платежей) + +
+ +### Последние квитанции — `lkcomu_interrao_last_invoice` + +> **Домен объектов:** `sensor` + +> @ TODO @ + +
+ +## Поддерживаемые ЛК + +Ниже предъявлен перечень поддерживаемых ЛК с их внутренними идентификаторами. +Данные идентификаторы используются как значение для поля `type`. + +> **Внимание:** Поддерживаются только ЛК физических лиц. Поддержка ЛК юридических лиц не планируется. +### ЕЛК ЖКХ (АО «Мосэнергосбыт», МосОблЕИРЦ, ПАО «Россети Московский регион») - `moscow` +[![Ссылка на личный кабинет "ЕЛК ЖКХ (АО «Мосэнергосбыт», МосОблЕИРЦ, ПАО «Россети Московский регион»)"](https://raw.githubusercontent.com/alryaz/hass-lkcomu-interrao/main/images/headers/moscow.png)](https://my.mosenergosbyt.ru) + +#### Пример конфигурации: + +```yaml +... +lkcomu_interrao: + type: moscow + username: username1 + password: password1 +``` + +#### Поставщик `MES` — Электричество +Для поставщика реализована поддержка следующих объектов: +
+ Информация о лицевом счёте + Скриншот +
+
+ Последний зарегистрированный платёж + Скриншот +
+
+ Последняя выпущенная квитанция + Скриншот +
+
+ Счётчик коммунальных услуг + Скриншот +
+ +#### Поставщик `KSG` — Электричество +Для поставщика реализована поддержка следующих объектов: +
+ Информация о лицевом счёте + Скриншот +
+
+ Последний зарегистрированный платёж + Скриншот +
+
+ Последняя выпущенная квитанция + Скриншот +
+
+ Счётчик коммунальных услуг + Скриншот +
+ +#### Поставщик `MOE` — ЕПД +Для поставщика реализована поддержка следующих объектов: +
+ Информация о лицевом счёте + Скриншот +
+
+ Последний зарегистрированный платёж + Скриншот +
+
+ Последняя выпущенная квитанция + Скриншот +
+
+ Счётчик коммунальных услуг + Скриншот +
+ +#### Поставщик `TKO` — ТКО +Для поставщика реализована поддержка следующих объектов: +
+ Информация о лицевом счёте + Скриншот +
+
+ Последний зарегистрированный платёж + Снимок экрана отсутствует +
+
+ Последняя выпущенная квитанция + Скриншот +
+
+ Счётчик коммунальных услуг + Снимок экрана отсутствует +
+ +### ЛКК Орел (ООО «Орловский энергосбыт») - `oryol` +[![Ссылка на личный кабинет "ЛКК Орел (ООО «Орловский энергосбыт»)"](https://raw.githubusercontent.com/alryaz/hass-lkcomu-interrao/main/images/headers/oryol.png)](https://my.interrao-orel.ru) + +#### Пример конфигурации: + +```yaml +... +lkcomu_interrao: + type: oryol + username: username1 + password: password1 +``` + +#### Поставщик `ORL_EPD` — ЕПД +Для поставщика реализована поддержка следующих объектов: +
+ Информация о лицевом счёте + Скриншот +
+
+ Последняя выпущенная квитанция + Скриншот +
+ +#### Поставщик `ORL` — Электричество +Для поставщика реализована поддержка следующих объектов: +
+ Информация о лицевом счёте + Скриншот +
+
+ Последний зарегистрированный платёж + Скриншот +
+
+ Последняя выпущенная квитанция + Скриншот +
+
+ Счётчик коммунальных услуг + Скриншот +
+ +### ЛКК ЭСВ (Энергосбыт Волга) - `volga` +[![Ссылка на личный кабинет "ЛКК ЭСВ (Энергосбыт Волга)"](https://raw.githubusercontent.com/alryaz/hass-lkcomu-interrao/main/images/headers/volga.png)](https://my.esbvolga.ru) + +#### Пример конфигурации: + +```yaml +... +lkcomu_interrao: + type: volga + username: username1 + password: password1 +``` + +#### Поставщик `VLD` — Электричество +Для поставщика реализована поддержка следующих объектов: +
+ Информация о лицевом счёте + Скриншот +
+
+ Последний зарегистрированный платёж + Скриншот +
+
+ Последняя выпущенная квитанция + Скриншот +
+
+ Счётчик коммунальных услуг + Скриншот +
+ +### ЕЛК Томск (Томскэнергосбыт / Томск РТС) - `tomsk` +[![Ссылка на личный кабинет "ЕЛК Томск (Томскэнергосбыт / Томск РТС)"](https://raw.githubusercontent.com/alryaz/hass-lkcomu-interrao/main/images/headers/tomsk.png)](https://my.tomskenergosbyt.ru) + +#### Пример конфигурации: + +```yaml +... +lkcomu_interrao: + type: tomsk + username: username1 + password: password1 +``` + +#### Поставщик `TMK_NRG` — generic +Для поставщика реализована поддержка следующих объектов: +
+ Информация о лицевом счёте + Скриншот +
+
+ Последний зарегистрированный платёж + Скриншот +
+
+ Последняя выпущенная квитанция + Скриншот +
+
+ Счётчик коммунальных услуг + Скриншот +
+ +### ЛК ТЭСК (Тамбовская энергосбытовая компания) - `tambov` +[![Ссылка на личный кабинет "ЛК ТЭСК (Тамбовская энергосбытовая компания)"](https://raw.githubusercontent.com/alryaz/hass-lkcomu-interrao/main/images/headers/tambov.png)](https://my.tesk.su) + +#### Пример конфигурации: + +```yaml +... +lkcomu_interrao: + type: tambov + username: username1 + password: password1 +``` + +#### Поставщик `TMB` — Электричество +Для поставщика реализована поддержка следующих объектов: +
+ Информация о лицевом счёте + Скриншот +
+
+ Последний зарегистрированный платёж + Скриншот +
+
+ Последняя выпущенная квитанция + Скриншот +
+
+ Счётчик коммунальных услуг + Скриншот +
+ +### ЕЛК Вологда (Северная сбытовая компания) - `sevesk` +[![Ссылка на личный кабинет "ЕЛК Вологда (Северная сбытовая компания)"](https://raw.githubusercontent.com/alryaz/hass-lkcomu-interrao/main/images/headers/sevesk.png)](https://lk.sevesk.ru) + +#### Пример конфигурации: + +```yaml +... +lkcomu_interrao: + type: sevesk + username: username1 + password: password1 +``` + +#### Поставщик `VLG` — Электричество +Для поставщика реализована поддержка следующих объектов: +
+ Информация о лицевом счёте + Скриншот +
+
+ Последний зарегистрированный платёж + Скриншот +
+
+ Последняя выпущенная квитанция + Скриншот +
+
+ Счётчик коммунальных услуг + Скриншот +
+ +### ЛК Саратов (ПАО «Саратовэнерго») - `saratov` +[![Ссылка на личный кабинет "ЛК Саратов (ПАО «Саратовэнерго»)"](https://raw.githubusercontent.com/alryaz/hass-lkcomu-interrao/main/images/headers/saratov.png)](https://my.saratovenergo.ru) + +#### Пример конфигурации: + +```yaml +... +lkcomu_interrao: + type: saratov + username: username1 + password: password1 +``` + +#### Поставщик `SAR` — Электричество +Для поставщика реализована поддержка следующих объектов: +
+ Информация о лицевом счёте + Скриншот +
+
+ Последний зарегистрированный платёж + Скриншот +
+
+ Последняя выпущенная квитанция + Скриншот +
+
+ Счётчик коммунальных услуг + Скриншот +
+ +### ЛКК ЭСКБ (Башэлектросбыт) - `bashkortostan` +[![Ссылка на личный кабинет "ЛКК ЭСКБ (Башэлектросбыт)"](https://raw.githubusercontent.com/alryaz/hass-lkcomu-interrao/main/images/headers/bashkortostan.png)](https://lkk.bashesk.ru) + +#### Пример конфигурации: + +```yaml +... +lkcomu_interrao: + type: bashkortostan + username: username1 + password: password1 +``` + +#### Поставщик `UFA` — Электричество +Для поставщика реализована поддержка следующих объектов: +
+ Информация о лицевом счёте + Скриншот +
+
+ Последний зарегистрированный платёж + Скриншот +
+
+ Последняя выпущенная квитанция + Скриншот +
+
+ Счётчик коммунальных услуг + Скриншот +
+ +### ЛК Алтай (АО «АлтайЭнергосбыт») - `altai` +[![Ссылка на личный кабинет "ЛК Алтай (АО «АлтайЭнергосбыт»)"](https://raw.githubusercontent.com/alryaz/hass-lkcomu-interrao/main/images/headers/altai.png)](https://lkfl.altaiensb.com) + +#### Пример конфигурации: + +```yaml +... +lkcomu_interrao: + type: altai + username: username1 + password: password1 +``` + +#### Поставщик `ALT` — Электричество +Для поставщика реализована поддержка следующих объектов: +
+ Информация о лицевом счёте + Скриншот +
+
+ Последний зарегистрированный платёж + Скриншот +
+
+ Последняя выпущенная квитанция + Скриншот +
+
+ Счётчик коммунальных услуг + Скриншот +
+ +## Дополнительная информация + +Компонент находится в активной разработке. Примерная дорожная карта и план развития: + +- `[0.0.1; 0.1.0)` — первичная обкатка компонента на поддерживаемых поставщиках; +- `[0.1.0; 0.2.0)` — разработка визуального конфигуратора для интеграции; +- `[0.2.0; 1.0.0)` — выполнение задач по оптимизации и чистке кода; +- `[1.0.0; .....)` — финальный выпуск компонента и исправление ошибок. + +Увеличение старшей версии сопутствует: + +- изменениям в конфигурационной схеме; +- удалению функционала; +- расширению сферы применения интеграции. + +Увеличение младшей версии сопутствует: + +- добавлению нового функционала; +- исправлению значительных ошибок. + +Увеличение номера сборки сопутствует: + +- исправлению мелких ошибок / выпуску хотфиксов; +- изменениям во взаимодействии с основополагающей библиотекой (или её версии). diff --git a/custom_components/lkcomu_interrao/_base.py b/custom_components/lkcomu_interrao/_base.py index 8a7e8ff..887f285 100644 --- a/custom_components/lkcomu_interrao/_base.py +++ b/custom_components/lkcomu_interrao/_base.py @@ -10,7 +10,6 @@ import asyncio import logging -import re from abc import abstractmethod from datetime import timedelta from typing import ( @@ -23,7 +22,6 @@ Iterable, List, Mapping, - MutableMapping, Optional, Set, SupportsInt, @@ -46,7 +44,7 @@ from homeassistant.helpers import entity_platform from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import ConfigType, HomeAssistantType, StateType +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util import as_local, utcnow from custom_components.lkcomu_interrao._util import ( @@ -61,7 +59,6 @@ ATTR_ACCOUNT_CODE, ATTR_ACCOUNT_ID, CONF_ACCOUNTS, - CONF_DEV_PRESENTATION, CONF_NAME_FORMAT, DATA_API_OBJECTS, DATA_ENTITIES, @@ -72,7 +69,6 @@ FORMAT_VAR_ACCOUNT_CODE, FORMAT_VAR_ACCOUNT_ID, FORMAT_VAR_CODE, - FORMAT_VAR_ID, FORMAT_VAR_PROVIDER_CODE, FORMAT_VAR_PROVIDER_NAME, SUPPORTED_PLATFORMS, @@ -150,9 +146,6 @@ async def async_register_update_delegator( await async_refresh_api_data(hass, config_entry) -DEV_CLASSES_PROCESSED = set() - - async def async_refresh_api_data(hass: HomeAssistantType, config_entry: ConfigEntry): entry_id = config_entry.entry_id api: "BaseEnergosbytAPI" = hass.data[DATA_API_OBJECTS][entry_id] @@ -204,19 +197,6 @@ async def async_refresh_api_data(hass: HomeAssistantType, config_entry: ConfigEn entities: EntitiesDataType = hass.data[DATA_ENTITIES][entry_id] final_config: ConfigType = dict(hass.data[DATA_FINAL_CONFIG][entry_id]) - dev_presentation = final_config.get(CONF_DEV_PRESENTATION) - dev_log_prefix = log_prefix_base + "[dev] " - - if dev_presentation: - from pprint import pformat - - _LOGGER.debug( - dev_log_prefix - + ("Конечная конфигурация:" if IS_IN_RUSSIA else "Final configuration:") - + "\n" - + pformat(final_config) - ) - platform_tasks = {} accounts_config = final_config.get(CONF_ACCOUNTS) or {} @@ -249,24 +229,6 @@ async def async_refresh_api_data(hass: HomeAssistantType, config_entry: ConfigEn ) continue - if dev_presentation: - dev_key = (entity_cls, account.provider_type) - if dev_key in DEV_CLASSES_PROCESSED: - _LOGGER.debug( - cls_log_prefix_base - + "[dev] " - + ( - f"Пропущен лицевой счёт ({mask_username(account.code)}) " - f"по уникальности типа" - if IS_IN_RUSSIA - else f"Account skipped ({mask_username(account.code)}) " - f"due to type uniqueness" - ) - ) - continue - - DEV_CLASSES_PROCESSED.add(dev_key) - current_entities = entities.setdefault(entity_cls, {}) _LOGGER.debug( @@ -396,39 +358,6 @@ def device_info(self) -> Dict[str, Any]: return device_info - def _handle_dev_presentation( - self, - mapping: MutableMapping[str, Any], - filter_vars: Iterable[str], - blackout_vars: Optional[Iterable[str]] = None, - ) -> None: - if self._account_config[CONF_DEV_PRESENTATION]: - filter_vars = set(filter_vars) - if blackout_vars is not None: - blackout_vars = set(blackout_vars) - filter_vars.difference_update(blackout_vars) - - for attr in blackout_vars: - value = mapping.get(attr) - if value is not None: - if isinstance(value, float): - value = "#####.###" - elif isinstance(value, int): - value = "#####" - elif isinstance(value, str): - value = "XXXXX" - else: - value = "*****" - mapping[attr] = value - - for attr in filter_vars: - value = mapping.get(attr) - if value is not None: - value = re.sub(r"[A-Za-z]", "X", str(value)) - value = re.sub(r"[0-9]", "#", value) - value = re.sub(r"\w+", "*", value) - mapping[attr] = value - ################################################################################# # Config getter helpers ################################################################################# @@ -477,11 +406,6 @@ def extra_state_attributes(self): if ATTR_ACCOUNT_CODE not in attributes: attributes[ATTR_ACCOUNT_CODE] = self._account.code - self._handle_dev_presentation( - attributes, - (ATTR_ACCOUNT_CODE, ATTR_ACCOUNT_ID), - ) - return attributes @property @@ -506,12 +430,6 @@ def name(self) -> Optional[str]: if FORMAT_VAR_PROVIDER_NAME not in name_format_values: name_format_values[FORMAT_VAR_PROVIDER_NAME] = self._account.provider_name - self._handle_dev_presentation( - name_format_values, - (FORMAT_VAR_CODE, FORMAT_VAR_ACCOUNT_CODE), - (FORMAT_VAR_ACCOUNT_ID, FORMAT_VAR_ID), - ) - return self.name_format.format_map(NameFormatDict(name_format_values)) ################################################################################# @@ -611,19 +529,8 @@ async def async_update_internal(self) -> None: raise NotImplementedError @property - @abstractmethod def code(self) -> str: - raise NotImplementedError - - @property - @abstractmethod - def state(self) -> StateType: - raise NotImplementedError - - @property - @abstractmethod - def icon(self) -> str: - raise NotImplementedError + return self._account.code @property @abstractmethod @@ -640,11 +547,6 @@ def name_format_values(self) -> Mapping[str, Any]: def unique_id(self) -> str: raise NotImplementedError - @property - @abstractmethod - def device_class(self) -> Optional[str]: - raise NotImplementedError - def register_supported_services(self, for_object: Optional[Any] = None) -> None: for type_feature, services in self._supported_services.items(): result, features = ( diff --git a/custom_components/lkcomu_interrao/_schema.py b/custom_components/lkcomu_interrao/_schema.py index c3004ad..64b9aa4 100644 --- a/custom_components/lkcomu_interrao/_schema.py +++ b/custom_components/lkcomu_interrao/_schema.py @@ -18,7 +18,6 @@ API_TYPE_DEFAULT, API_TYPE_NAMES, CONF_ACCOUNTS, - CONF_DEV_PRESENTATION, CONF_LAST_INVOICE, CONF_LAST_PAYMENT, CONF_LOGOS, @@ -96,7 +95,6 @@ def _validator_name_format_schema(schema): vol.Optional(CONF_METERS, default=True): cv.boolean, vol.Optional(CONF_LAST_PAYMENT, default=True): cv.boolean, vol.Optional(CONF_LOGOS, default=True): cv.boolean, - vol.Optional(CONF_DEV_PRESENTATION, default=False): cv.boolean, vol.Optional(CONF_NAME_FORMAT, default=lambda: NAME_FORMAT_SCHEMA({})): vol.Any( vol.All(cv.string, lambda x: {CONF_ACCOUNTS: x}, NAME_FORMAT_SCHEMA), NAME_FORMAT_SCHEMA, @@ -161,7 +159,6 @@ def _make_provider_schema( vol.Required(CONF_TYPE): PROFILE_TYPE_VALIDATOR, vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_DEV_PRESENTATION, default=False): cv.boolean, # Additional API configuration vol.Optional(CONF_USER_AGENT): vol.All( cv.string, lambda x: " ".join(map(str.strip, x.split("\n"))) diff --git a/custom_components/lkcomu_interrao/binary_sensor.py b/custom_components/lkcomu_interrao/binary_sensor.py index 45a397b..6943d5d 100644 --- a/custom_components/lkcomu_interrao/binary_sensor.py +++ b/custom_components/lkcomu_interrao/binary_sensor.py @@ -2,12 +2,7 @@ from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - STATE_OFF, - STATE_ON, - STATE_UNKNOWN, -) -from homeassistant.helpers.typing import ConfigType, StateType +from homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify from custom_components.lkcomu_interrao._base import ( @@ -16,13 +11,7 @@ ) from custom_components.lkcomu_interrao._encoders import payment_to_attrs from custom_components.lkcomu_interrao.const import ( - ATTR_AGENT, - ATTR_AMOUNT, - ATTR_GROUP, - ATTR_PAID_AT, - ATTR_PERIOD, CONF_LAST_PAYMENT, - DOMAIN, FORMAT_VAR_ID, FORMAT_VAR_TYPE_EN, FORMAT_VAR_TYPE_RU, @@ -35,6 +24,8 @@ class LkcomuInterRAOLastPayment( LkcomuInterRAOEntity[AbstractAccountWithPayments], BinarySensorEntity ): + _attr_icon = "mdi:cash-multiple" + config_key: ClassVar[str] = CONF_LAST_PAYMENT def __init__(self, *args, last_payment: Optional[AbstractPayment] = None, **kwargs) -> None: @@ -46,9 +37,11 @@ def __init__(self, *args, last_payment: Optional[AbstractPayment] = None, **kwar ) @property - def is_on(self) -> bool: + def is_on(self) -> Optional[bool]: payment = self._last_payment - return payment is not None and payment.is_accepted + if payment is None: + return None + return payment.is_accepted @property def entity_id(self) -> Optional[str]: @@ -92,37 +85,11 @@ async def async_update_internal(self) -> None: # Data-oriented implementation of inherent class ################################################################################# - @property - def code(self) -> str: - return self._account.code - - @property - def state(self) -> StateType: - data = self._last_payment - - if data is None: - return STATE_UNKNOWN - - return STATE_ON if self.is_on else STATE_OFF - - @property - def icon(self) -> str: - return "mdi:cash-multiple" - @property def sensor_related_attributes(self) -> Optional[Mapping[str, Any]]: payment = self._last_payment - - if payment is None: - attributes = {} - else: - - attributes = payment_to_attrs(payment) - self._handle_dev_presentation( - attributes, (ATTR_PAID_AT, ATTR_PERIOD), (ATTR_AMOUNT, ATTR_AGENT, ATTR_GROUP) - ) - - return attributes + if payment: + return payment_to_attrs(payment) @property def name_format_values(self) -> Mapping[str, Any]: @@ -139,9 +106,5 @@ def unique_id(self) -> str: acc = self._account return f"{acc.api.__class__.__name__}_lastpayment_{acc.id}" - @property - def device_class(self) -> Optional[str]: - return DOMAIN + "_payment" - async_setup_entry = make_common_async_setup_entry(LkcomuInterRAOLastPayment) diff --git a/custom_components/lkcomu_interrao/config_flow.py b/custom_components/lkcomu_interrao/config_flow.py index 57d4cbc..3a06af4 100644 --- a/custom_components/lkcomu_interrao/config_flow.py +++ b/custom_components/lkcomu_interrao/config_flow.py @@ -8,8 +8,6 @@ Any, ClassVar, Dict, - Iterable, - List, Mapping, Optional, TYPE_CHECKING, @@ -19,12 +17,10 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.config_entries import ConfigFlow from homeassistant.const import ( CONF_DEFAULT, - CONF_ENTITIES, CONF_PASSWORD, - CONF_SCAN_INTERVAL, CONF_TYPE, CONF_USERNAME, ) @@ -36,25 +32,18 @@ API_TYPE_DEFAULT, API_TYPE_NAMES, CONF_ACCOUNTS, - CONF_LAST_INVOICE, - CONF_METERS, - CONF_NAME_FORMAT, CONF_USER_AGENT, - DATA_API_OBJECTS, - DATA_ENTITIES, DOMAIN, ) from inter_rao_energosbyt.const import DEFAULT_USER_AGENT from inter_rao_energosbyt.exceptions import EnergosbytException from inter_rao_energosbyt.interfaces import ( - AbstractAccountWithMeters, - AbstractMeter, Account, BaseEnergosbytAPI, ) if TYPE_CHECKING: - from custom_components.lkcomu_interrao._base import LkcomuInterRAOEntity + pass _LOGGER = logging.getLogger(__name__) @@ -236,276 +225,3 @@ async def async_step_import(self, user_input: Optional[ConfigType] = None) -> Di # @callback # def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: # return Inter RAOOptionsFlow(config_entry) - - -CONF_DISABLE_ACCOUNTS = "disable_" + CONF_ACCOUNTS -CONF_DISABLE_METERS = "disable_" + CONF_METERS -CONF_DISABLE_INVOICES = "disable_" + CONF_LAST_INVOICE -CONF_USE_TEXT_FIELDS = "use_text_fields" - - -class InterRAOOptionsFlow(OptionsFlow): - """Handler for Inter RAO options""" - - def __init__(self, config_entry: ConfigEntry): - self.config_entry = config_entry - self.use_text_fields = False - self.config_codes: Optional[Dict[str, List[str]]] = None - - async def async_fetch_config_codes(self): - api: "BaseEnergosbytAPI" = self.hass.data[DATA_API_OBJECTS][self.config_entry.entry_id] - accounts = await api.async_update_accounts(with_related=True) - account_codes = {account.code for account in accounts.values() if account.code is not None} - - aws = ( - account.async_get_meters() - for account in accounts - if isinstance(account, AbstractAccountWithMeters) - ) - - meters_maps: Iterable[Mapping[int, "AbstractMeter"]] = await asyncio.gather(*aws) - meter_codes = set() - - for meters_map in meters_maps: - meter_codes.update( - [meter.code for meter in meters_map.values() if meter.code is not None] - ) - - return { - CONF_ACCOUNTS: sorted(account_codes), - CONF_LAST_INVOICE: sorted(account_codes), - CONF_METERS: sorted(meter_codes), - } - - async def async_get_options_multiselect(self, config_key: str) -> Dict[str, str]: - if self.config_codes is None: - try: - self.config_codes = await self.async_fetch_config_codes() - config_codes = self.config_codes - except EnergosbytException: - self.use_text_fields = True - config_codes = {} - - else: - config_codes = self.config_codes - - options = OrderedDict() - - entities: List["LkcomuInterRAOEntity"] = ( - self.hass.data.get(DATA_ENTITIES, {}) - .get(self.config_entry.entry_id, {}) - .get(config_key, []) - ) - - for code in sorted(config_codes.get(config_key, [])): - text = code - - for entity in entities: - if entity.code == code: - text += " (" + entity.entity_id + ")" - break - - options[code] = text - - return options - - async def async_generate_schema_dict( - self, user_input: Optional[ConfigType] = None - ) -> OrderedDict: - user_input = user_input or {} - - schema_dict = OrderedDict() - - all_cfg = {**self.config_entry.data} - - if self.config_entry.options: - all_cfg.update(self.config_entry.options) - - # Entity filtering - try: - option_entities = ENTITY_CONF_VALIDATORS[CONF_ENTITIES](all_cfg.get(CONF_ENTITIES, {})) - except vol.Invalid: - option_entities = ENTITY_CONF_VALIDATORS[CONF_ENTITIES]({}) - - async def _add_filter(config_key_: str): - filter_key = CONF_ENTITIES + "_" + config_key_ - blacklist_key = filter_key + "_blacklist" - - default_value = vol.UNDEFINED - blacklisted = True - - if filter_key in user_input: - default_value = user_input[filter_key] - - else: - options_value = option_entities.get(config_key_) - - if options_value: - blacklisted = options_value[CONF_DEFAULT] - - default_value = [ - key - for key, value in options_value.items() - if key != CONF_DEFAULT and value is not blacklisted - ] - - if self.use_text_fields: - # Validate text for text fields - validator = cv.string - - if default_value is not vol.UNDEFINED and isinstance(default_value, list): - default_value = ",".join(default_value) - else: - # Validate options for multi-select fields - select_options = await self.async_get_options_multiselect(config_key_) - - if default_value is not vol.UNDEFINED: - if isinstance(default_value, str): - default_value = list(map(str.strip, default_value.split(","))) - - for value in default_value: - if value not in select_options: - select_options[value] = value - - validator = cv.multi_select(select_options) - - schema_dict[vol.Optional(filter_key, default=default_value)] = validator - schema_dict[vol.Optional(blacklist_key, default=blacklisted)] = cv.boolean - - # Scan intervals - try: - option_scan_interval = ENTITY_CONF_VALIDATORS[CONF_SCAN_INTERVAL]( - all_cfg.get(CONF_SCAN_INTERVAL, {}) - ) - except vol.Invalid: - option_scan_interval = ENTITY_CONF_VALIDATORS[CONF_SCAN_INTERVAL]({}) - - async def _add_scan_interval(config_key_: str): - scan_interval_key = CONF_SCAN_INTERVAL + "_" + config_key_ - - if scan_interval_key in user_input: - default_value = user_input[scan_interval_key] - - else: - default_value = option_scan_interval[config_key_][CONF_DEFAULT] - - if isinstance(default_value, timedelta): - default_value = default_value.total_seconds() - - default_value = { - "seconds": default_value % 60, - "minutes": default_value % (60 * 60) // 60, - "hours": default_value % (60 * 60 * 24) // (60 * 60), - } - - schema_dict[ - vol.Optional(scan_interval_key, default=default_value) - ] = cv.positive_time_period_dict - - # Name formats - try: - option_name_format = ENTITY_CONF_VALIDATORS[CONF_NAME_FORMAT]( - all_cfg.get(CONF_NAME_FORMAT, {}) - ) - except vol.Invalid: - option_name_format = ENTITY_CONF_VALIDATORS[CONF_NAME_FORMAT]({}) - - async def _add_name_format(config_key_: str): - name_format_key = CONF_NAME_FORMAT + "_" + config_key_ - name_format_value = user_input.get(name_format_key) - - if name_format_value is None: - name_format_value = option_name_format[config_key_][CONF_DEFAULT] - - schema_dict[vol.Optional(name_format_key, default=name_format_value)] = cv.string - - for config_key in ENTITY_CODES_VALIDATORS.keys(): - await _add_filter(config_key) - await _add_scan_interval(config_key) - await _add_name_format(config_key) - - schema_dict[vol.Optional(CONF_USE_TEXT_FIELDS, default=self.use_text_fields)] = cv.boolean - - default_user_agent = all_cfg.get(CONF_USER_AGENT) or DEFAULT_USER_AGENT - schema_dict[vol.Optional(CONF_USER_AGENT, default=default_user_agent)] = cv.string - - return schema_dict - - async def async_step_init(self, user_input: Optional[ConfigType] = None) -> Dict[str, Any]: - if self.config_entry.source == config_entries.SOURCE_IMPORT: - return self.async_abort(reason="yaml_not_supported") - - errors = {} - if user_input: - use_text_fields = user_input.get(CONF_USE_TEXT_FIELDS, self.use_text_fields) - if use_text_fields == self.use_text_fields: - new_options = {} - - if CONF_USER_AGENT in user_input: - new_options[CONF_USER_AGENT] = user_input[CONF_USER_AGENT] - - def _save_filter(config_key_: str): - filter_key = CONF_ENTITIES + "_" + config_key_ - blacklist_key = filter_key + "_blacklist" - - value = user_input.get(filter_key) - - if value is None: - value = [] - elif isinstance(value, str): - value = list(filter(bool, map(str.strip, value.split(",")))) - - if CONF_DEFAULT in value: - errors[filter_key] = "value_default_not_valid" - return - - blacklisted = user_input[blacklist_key] - - try: - codes = list(map(validator, value)) - - except vol.Invalid as e: - _LOGGER.error("Error parsing options: %s", e) - errors[config_key_] = "invalid_code_format" - return - - else: - entities_options = new_options.setdefault(CONF_ENTITIES, {}) - entities_options[config_key_] = dict.fromkeys(codes, not blacklisted) - entities_options[config_key_][CONF_DEFAULT] = blacklisted - - def _save_scan_interval(config_key_: str): - scan_interval_key = CONF_SCAN_INTERVAL + "_" + config_key_ - scan_interval_value = user_input.get(scan_interval_key) - - if scan_interval_value is not None: - scan_interval_options = new_options.setdefault(CONF_SCAN_INTERVAL, {}) - scan_interval_options[config_key_] = int( - scan_interval_value.total_seconds() - ) - - def _save_name_format(config_key_: str): - name_format_key = CONF_NAME_FORMAT + "_" + config_key_ - name_format_value = user_input.get(name_format_key) - - if name_format_value is not None: - name_format_options = new_options.setdefault(CONF_NAME_FORMAT, {}) - name_format_options[config_key_] = str(name_format_value).strip() - - for config_key, validator in ENTITY_CODES_VALIDATORS.items(): - _save_filter(config_key) - _save_scan_interval(config_key) - _save_name_format(config_key) - - if not errors: - _LOGGER.debug("Saving options: %s", new_options) - return self.async_create_entry(title="", data=new_options) - - else: - self.use_text_fields = use_text_fields - - schema_dict = await self.async_generate_schema_dict(user_input) - - return self.async_show_form( - step_id="init", data_schema=vol.Schema(schema_dict), errors=errors or None - ) diff --git a/custom_components/lkcomu_interrao/const.py b/custom_components/lkcomu_interrao/const.py index 9070acf..71b52d5 100644 --- a/custom_components/lkcomu_interrao/const.py +++ b/custom_components/lkcomu_interrao/const.py @@ -65,7 +65,6 @@ ATTR_UNIT: Final = "unit" CONF_ACCOUNTS: Final = "accounts" -CONF_DEV_PRESENTATION: Final = "dev_presentation" CONF_LAST_INVOICE: Final = "last_invoice" CONF_LAST_PAYMENT: Final = "last_payment" CONF_LOGOS: Final = "logos" diff --git a/custom_components/lkcomu_interrao/sensor.py b/custom_components/lkcomu_interrao/sensor.py index 04d14b5..7f19d04 100644 --- a/custom_components/lkcomu_interrao/sensor.py +++ b/custom_components/lkcomu_interrao/sensor.py @@ -1,1014 +1,931 @@ -""" -Sensor for Inter RAO cabinet. -Retrieves indications regarding current state of accounts. -""" -import logging -import re -from datetime import date, datetime -from enum import IntEnum -from typing import ( - Any, - ClassVar, - Dict, - Final, - Hashable, - Mapping, - Optional, - TypeVar, - Union, -) - -import voluptuous as vol -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_SERVICE, - CONF_DESCRIPTION, - STATE_LOCKED, - STATE_OK, - STATE_PROBLEM, - STATE_UNKNOWN, -) -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.typing import ConfigType -from homeassistant.util import slugify - -from custom_components.lkcomu_interrao._base import ( - LkcomuInterRAOEntity, - SupportedServicesType, - make_common_async_setup_entry, -) -from custom_components.lkcomu_interrao._encoders import invoice_to_attrs, payment_to_attrs -from custom_components.lkcomu_interrao._util import with_auto_auth -from custom_components.lkcomu_interrao.const import ( - ATTR_ACCOUNT_CODE, - ATTR_ACCOUNT_ID, - ATTR_ADDRESS, - ATTR_BENEFITS, - ATTR_CALL_PARAMS, - ATTR_CHARGED, - ATTR_COMMENT, - ATTR_DESCRIPTION, - ATTR_END, - ATTR_FULL_NAME, - ATTR_IGNORE_INDICATIONS, - ATTR_IGNORE_PERIOD, - ATTR_INCREMENTAL, - ATTR_INDICATIONS, - ATTR_INITIAL, - ATTR_INSTALL_DATE, - ATTR_INSURANCE, - ATTR_INVOICE_ID, - ATTR_LAST_INDICATIONS_DATE, - ATTR_LIVING_AREA, - ATTR_METER_CATEGORY, - ATTR_METER_CODE, - ATTR_METER_MODEL, - ATTR_MODEL, - ATTR_PAID, - ATTR_PENALTY, - ATTR_PERIOD, - ATTR_PREVIOUS, - ATTR_PROVIDER_NAME, - ATTR_PROVIDER_TYPE, - ATTR_REASON, - ATTR_REMAINING_DAYS, - ATTR_RESULT, - ATTR_SERVICE_NAME, - ATTR_SERVICE_TYPE, - ATTR_START, - ATTR_STATUS, - ATTR_SUBMIT_PERIOD_ACTIVE, - ATTR_SUBMIT_PERIOD_END, - ATTR_SUBMIT_PERIOD_START, - ATTR_SUCCESS, - ATTR_SUM, - ATTR_TOTAL, - ATTR_TOTAL_AREA, - CONF_ACCOUNTS, - CONF_DEV_PRESENTATION, - CONF_LAST_INVOICE, - CONF_LOGOS, - CONF_METERS, - DATA_PROVIDER_LOGOS, - DOMAIN, - FORMAT_VAR_ID, - FORMAT_VAR_TYPE_EN, - FORMAT_VAR_TYPE_RU, -) -from inter_rao_energosbyt.exceptions import EnergosbytException -from inter_rao_energosbyt.interfaces import ( - AbstractAccountWithBalance, - AbstractAccountWithInvoices, - AbstractAccountWithMeters, - AbstractAccountWithPayments, - AbstractBalance, - AbstractCalculatableMeter, - AbstractInvoice, - AbstractMeter, - AbstractSubmittableMeter, - Account, -) -from inter_rao_energosbyt.presets.byt import AccountWithBytInfo, BytInfoSingle -from inter_rao_energosbyt.util import process_start_end_arguments - -_LOGGER = logging.getLogger(__name__) - -RE_HTML_TAGS = re.compile(r"<[^<]+?>") -RE_MULTI_SPACES = re.compile(r"\s{2,}") - -INDICATIONS_MAPPING_SCHEMA = vol.Schema( - { - vol.Required(vol.Match(r"t\d+")): cv.positive_float, - } -) - -INDICATIONS_SEQUENCE_SCHEMA = vol.All( - vol.Any(vol.All(cv.positive_float, cv.ensure_list), [cv.positive_float]), - lambda x: dict(map(lambda y: ("t" + str(y[0]), y[1]), enumerate(x, start=1))), -) - -CALCULATE_PUSH_INDICATIONS_SCHEMA = vol.All( - cv.deprecated("notification"), - cv.make_entity_service_schema({ - vol.Required(ATTR_INDICATIONS): vol.Any( - vol.All( - cv.string, lambda x: list(map(str.strip, x.split(","))), INDICATIONS_SEQUENCE_SCHEMA - ), - INDICATIONS_MAPPING_SCHEMA, - INDICATIONS_SEQUENCE_SCHEMA, - ), - vol.Optional(ATTR_IGNORE_PERIOD, default=False): cv.boolean, - vol.Optional(ATTR_IGNORE_INDICATIONS, default=False): cv.boolean, - vol.Optional(ATTR_INCREMENTAL, default=False): cv.boolean, - vol.Optional("notification", default=None): lambda x: x, - }) -) - -SERVICE_PUSH_INDICATIONS: Final = "push_indications" -SERVICE_PUSH_INDICATIONS_SCHEMA: Final = CALCULATE_PUSH_INDICATIONS_SCHEMA - -SERVICE_CALCULATE_INDICATIONS: Final = "calculate_indications" -SERVICE_CALCULATE_INDICATIONS_SCHEMA: Final = CALCULATE_PUSH_INDICATIONS_SCHEMA - -_SERVICE_SCHEMA_BASE_DATED: Final = { - vol.Optional(ATTR_START, default=None): vol.Any(vol.Equal(None), cv.datetime), - vol.Optional(ATTR_END, default=None): vol.Any(vol.Equal(None), cv.datetime), -} - -FEATURE_PUSH_INDICATIONS: Final = 1 -FEATURE_CALCULATE_INDICATIONS: Final = FEATURE_PUSH_INDICATIONS * 2 -FEATURE_GET_PAYMENTS: Final = FEATURE_CALCULATE_INDICATIONS * 2 -FEATURE_GET_INVOICES: Final = FEATURE_GET_PAYMENTS * 2 - -SERVICE_SET_DESCRIPTION: Final = "set_description" -SERVICE_GET_PAYMENTS: Final = "get_payments" -SERVICE_GET_INVOICES: Final = "get_invoices" - -_TLkcomuInterRAOEntity = TypeVar("_TLkcomuInterRAOEntity", bound=LkcomuInterRAOEntity) - - -def get_supported_features(from_services: SupportedServicesType, for_object: Any) -> int: - features = 0 - for type_feature, services in from_services.items(): - if type_feature is None: - continue - check_cls, feature = type_feature - if isinstance(for_object, check_cls): - features |= feature - - return features - - -class LkcomuAccount(LkcomuInterRAOEntity[Account]): - """The class for this sensor""" - - config_key: ClassVar[str] = CONF_ACCOUNTS - - _supported_services: ClassVar[SupportedServicesType] = { - None: { - "set_description": { - vol.Optional(CONF_DESCRIPTION): vol.Any(vol.Equal(None), cv.string), - }, - }, - (AbstractAccountWithInvoices, FEATURE_GET_INVOICES): { - "get_invoices": _SERVICE_SCHEMA_BASE_DATED, - }, - (AbstractAccountWithPayments, FEATURE_GET_PAYMENTS): { - "get_payments": _SERVICE_SCHEMA_BASE_DATED, - }, - } - - def __init__(self, *args, balance: Optional[AbstractBalance] = None, **kwargs) -> None: - super().__init__(*args, *kwargs) - self._balance = balance - - self.entity_id: Optional[str] = f"sensor." + slugify( - f"{self.account_provider_code or 'unknown'}_{self._account.code}_account" - ) - - @property - def entity_picture(self) -> Optional[str]: - if not self._account_config[CONF_LOGOS]: - return None - - logos = self.hass.data.get(DATA_PROVIDER_LOGOS) - if not logos: - return None - - account_provider_code = self.account_provider_code - if account_provider_code is None: - return None - - provider_logo = logos.get(account_provider_code) - if isinstance(provider_logo, str): - return provider_logo - - return None - - @property - def code(self) -> str: - return self._account.code - - @property - def device_class(self) -> Optional[str]: - return DOMAIN + "_account" - - @property - def unique_id(self) -> str: - """Return the unique ID of the sensor""" - acc = self._account - return f"{acc.api.__class__.__name__}_account_{acc.id}" - - @property - def state(self) -> Union[str, float]: - if self._account.is_locked: - return STATE_PROBLEM - balance = self._balance - if balance is not None: - if self._account_config[CONF_DEV_PRESENTATION]: - return ("-" if (balance.balance or 0.0) < 0.0 else "") + "#####.###" - return round(balance.balance or 0.0, 2) # fixes -0.0 issues - return STATE_UNKNOWN - - @property - def icon(self) -> str: - return "mdi:lightning-bolt-circle" - - @property - def unit_of_measurement(self) -> Optional[str]: - return "руб." - - @property - def sensor_related_attributes(self) -> Optional[Mapping[str, Any]]: - account = self._account - service_type_value = account.service_type - service_type = ( - service_type_value.name.lower() - if isinstance(service_type_value, IntEnum) - else str(service_type_value) - ) - - provider_type_value = account.provider_type - provider_type = ( - provider_type_value.name.lower() - if isinstance(provider_type_value, IntEnum) - else str(provider_type_value) - ) - - attributes = { - ATTR_ADDRESS: account.address, - ATTR_DESCRIPTION: account.description, - ATTR_PROVIDER_TYPE: provider_type, - ATTR_PROVIDER_NAME: account.provider_name, - ATTR_SERVICE_TYPE: service_type, - ATTR_SERVICE_NAME: account.service_name, - } - - if account.is_locked: - attributes[ATTR_STATUS] = STATE_LOCKED - attributes[ATTR_REASON] = account.lock_reason - - else: - attributes[ATTR_STATUS] = STATE_OK - - if isinstance(account, AccountWithBytInfo): - info = account.info - if info: - attributes.update( - { - ATTR_FULL_NAME: info.full_name, - ATTR_LIVING_AREA: info.living_area, - ATTR_TOTAL_AREA: info.total_area, - ATTR_METER_CATEGORY: info.meter_category, - ATTR_METER_CODE: info.meter_code, - } - ) - - zones = account.info.zones - if zones is not None: - for zone_id, zone_def in zones.items(): - attrs = ("name", "description", "tariff") - for prefix in ("", "within_"): - values = tuple(getattr(zone_def, prefix + attr) for attr in attrs) - if any(values): - attributes.update( - zip( - map(lambda x: f"zone_{zone_id}_{prefix}{x}", attrs), - values, - ) - ) - - if isinstance(info, BytInfoSingle): - attributes[ATTR_METER_MODEL] = info.meter_model - - self._handle_dev_presentation( - attributes, - (), - ( - ATTR_DESCRIPTION, - ATTR_FULL_NAME, - ATTR_ADDRESS, - ATTR_LIVING_AREA, - ATTR_TOTAL_AREA, - ATTR_METER_MODEL, - ATTR_METER_CODE, - ), - ) - - return attributes - - @property - def name_format_values(self) -> Mapping[str, Any]: - """Return the name of the sensor""" - account = self._account - return { - FORMAT_VAR_ID: str(account.id), - FORMAT_VAR_TYPE_EN: "account", - FORMAT_VAR_TYPE_RU: "лицевой счёт", - } - - ################################################################################# - # Functional implementation of inherent class - ################################################################################# - - @classmethod - async def async_refresh_accounts( - cls, - entities: Dict[Hashable, _TLkcomuInterRAOEntity], - account: "Account", - config_entry: ConfigEntry, - account_config: ConfigType, - ): - entity_key = account.id - try: - entity = entities[entity_key] - except KeyError: - entity = cls(account, account_config) - entities[entity_key] = entity - - return [entity] - else: - if entity.enabled: - entity.async_schedule_update_ha_state(force_refresh=True) - - async def async_update_internal(self) -> None: - await self._account.async_update_related() - account = self._account - - if isinstance(account, AbstractAccountWithBalance): - self._balance = await account.async_get_balance() - - if isinstance(account, AccountWithBytInfo): - await account.async_update_info() - - self.register_supported_services(account) - - ################################################################################# - # Services callbacks - ################################################################################# - - @property - def supported_features(self) -> int: - return get_supported_features( - self._supported_services, - self._account, - ) - - async def async_service_get_payments(self, **call_data): - account = self._account - - _LOGGER.info(self.log_prefix + "Begin handling payments retrieval") - - if not isinstance(account, AbstractAccountWithPayments): - raise ValueError("account does not support payments retrieval") - - dt_start: Optional["datetime"] = call_data[ATTR_START] - dt_end: Optional["datetime"] = call_data[ATTR_END] - - dt_start, dt_end = process_start_end_arguments(dt_start, dt_end) - results = [] - - event_data = { - ATTR_ACCOUNT_CODE: account.code, - ATTR_ACCOUNT_ID: account.id, - ATTR_SUCCESS: False, - ATTR_START: dt_start.isoformat(), - ATTR_END: dt_end.isoformat(), - ATTR_RESULT: results, - ATTR_COMMENT: None, - ATTR_SUM: 0.0, - } - - try: - payments = await with_auto_auth( - account.api, - account.async_get_payments, - dt_start, - dt_end, - ) - - for payment in payments: - event_data[ATTR_SUM] += payment.amount - results.append(payment_to_attrs(payment)) - - except BaseException as e: - event_data[ATTR_COMMENT] = "Unknown error: %r" % e - _LOGGER.exception(event_data[ATTR_COMMENT]) - raise - else: - event_data[ATTR_SUCCESS] = True - - finally: - self.hass.bus.async_fire( - event_type=DOMAIN + "_" + SERVICE_GET_PAYMENTS, - event_data=event_data, - ) - - _LOGGER.info(self.log_prefix + "Finish handling payments retrieval") - - async def async_service_get_invoices(self, **call_data): - account = self._account - - _LOGGER.info(self.log_prefix + "Begin handling invoices retrieval") - - if not isinstance(account, AbstractAccountWithInvoices): - raise ValueError("account does not support invoices retrieval") - - dt_start: Optional["datetime"] = call_data[ATTR_START] - dt_end: Optional["datetime"] = call_data[ATTR_END] - - dt_start, dt_end = process_start_end_arguments(dt_start, dt_end) - results = [] - - event_data = { - ATTR_ACCOUNT_CODE: account.code, - ATTR_ACCOUNT_ID: account.id, - ATTR_SUCCESS: False, - ATTR_START: dt_start.isoformat(), - ATTR_END: dt_end.isoformat(), - ATTR_RESULT: results, - ATTR_COMMENT: None, - ATTR_SUM: 0.0, - } - - try: - invoices = await with_auto_auth( - account.api, - account.async_get_invoices, - dt_start, - dt_end, - ) - - for invoice in invoices: - event_data[ATTR_SUM] += invoice.total - results.append(invoice_to_attrs(invoice)) - - except BaseException as e: - event_data[ATTR_COMMENT] = "Unknown error: %r" % e - _LOGGER.exception(event_data[ATTR_COMMENT]) - raise - else: - event_data[ATTR_SUCCESS] = True - - finally: - self.hass.bus.async_fire( - event_type=DOMAIN + "_" + SERVICE_GET_INVOICES, - event_data=event_data, - ) - - _LOGGER.info(self.log_prefix + "Finish handling invoices retrieval") - - async def async_service_set_description(self, **call_data): - account = self._account - - _LOGGER.info(self.log_prefix + "Begin handling description setting") - - event_data = { - ATTR_ACCOUNT_CODE: account.code, - ATTR_ACCOUNT_ID: account.id, - ATTR_SUCCESS: False, - ATTR_DESCRIPTION: call_data.get(CONF_DESCRIPTION), - ATTR_PREVIOUS: account.description, - } - - try: - await with_auto_auth( - account.api, - account.async_set_description, - description=event_data[ATTR_DESCRIPTION], - update=False, - ) - - except EnergosbytException as e: - event_data[ATTR_COMMENT] = "Error: %s" % e - raise - - except Exception as e: - event_data[ATTR_COMMENT] = "Unknown error: %s" % e - _LOGGER.exception("Unknown error: %s", e) - raise - - else: - event_data[ATTR_COMMENT] = "Successful calculation" - event_data[ATTR_SUCCESS] = True - self.async_schedule_update_ha_state(force_refresh=True) - - finally: - self.hass.bus.async_fire( - event_type=DOMAIN + "_" + SERVICE_SET_DESCRIPTION, - event_data=event_data, - ) - - _LOGGER.info(self.log_prefix + "End handling indications calculation") - - -class LkcomuMeter(LkcomuInterRAOEntity[AbstractAccountWithMeters]): - """The class for this sensor""" - - config_key: ClassVar[str] = CONF_METERS - - _supported_services: ClassVar[SupportedServicesType] = { - (AbstractSubmittableMeter, FEATURE_PUSH_INDICATIONS): { - "push_indications": SERVICE_PUSH_INDICATIONS_SCHEMA, - }, - (AbstractCalculatableMeter, FEATURE_CALCULATE_INDICATIONS): { - "calculate_indications": SERVICE_CALCULATE_INDICATIONS_SCHEMA, - }, - } - - def __init__(self, *args, meter: AbstractMeter, **kwargs) -> None: - super().__init__(*args, **kwargs) - self._meter = meter - - self.entity_id: Optional[str] = f"sensor." + slugify( - f"{self.account_provider_code or 'unknown'}_{self._account.code}_meter_{self.code}" - ) - - ################################################################################# - # Implementation base of inherent class - ################################################################################# - - @classmethod - async def async_refresh_accounts( - cls, - entities: Dict[Hashable, Optional[_TLkcomuInterRAOEntity]], - account: "Account", - config_entry: ConfigEntry, - account_config: ConfigType, - ): - new_meter_entities = [] - if isinstance(account, AbstractAccountWithMeters): - meters = await account.async_get_meters() - - for meter_id, meter in meters.items(): - entity_key = (account.id, meter_id) - try: - entity = entities[entity_key] - except KeyError: - entity = cls( - account, - account_config, - meter=meter, - ) - entities[entity_key] = entity - new_meter_entities.append(entity) - else: - if entity.enabled: - entity.async_schedule_update_ha_state(force_refresh=True) - - return new_meter_entities if new_meter_entities else None - - async def async_update_internal(self) -> None: - meters = await self._account.async_get_meters() - meter = meters.get(self._meter.id) - if meter is None: - self.hass.async_create_task(self.async_remove()) - else: - self.register_supported_services(meter) - - self._meter = meter - - ################################################################################# - # Data-oriented implementation of inherent class - ################################################################################# - - @property - def code(self) -> str: - return self._meter.code - - @property - def unique_id(self) -> str: - """Return the unique ID of the sensor""" - met = self._meter - acc = met.account - return f"{acc.api.__class__.__name__}_meter_{acc.id}_{met.id}" - - @property - def state(self) -> str: - return self._meter.status or STATE_OK - - @property - def icon(self): - return "mdi:counter" - - @property - def device_class(self) -> Optional[str]: - return DOMAIN + "_meter" - - @property - def supported_features(self) -> int: - meter = self._meter - return ( - isinstance(meter, AbstractSubmittableMeter) * FEATURE_PUSH_INDICATIONS - | isinstance(meter, AbstractCalculatableMeter) * FEATURE_CALCULATE_INDICATIONS - ) - - @property - def sensor_related_attributes(self) -> Optional[Mapping[str, Any]]: - met = self._meter - attributes = { - ATTR_METER_CODE: met.code, - ATTR_ACCOUNT_CODE: met.account.code, - } - - # Meter model attribute - model = met.model - if model: - attributes[ATTR_MODEL] = model - - # Installation date attribute - install_date = met.installation_date - if install_date: - attributes[ATTR_INSTALL_DATE] = install_date.isoformat() - - # Submission periods attributes - is_submittable = False - if isinstance(met, AbstractSubmittableMeter): - is_submittable = True # this weird hack calms my IDE - - # noinspection PyUnresolvedReferences - today = date.today() - start_date, end_date = met.submission_period - attributes[ATTR_SUBMIT_PERIOD_START] = start_date.isoformat() - attributes[ATTR_SUBMIT_PERIOD_END] = end_date.isoformat() - attributes[ATTR_SUBMIT_PERIOD_ACTIVE] = start_date <= today <= end_date - - if date.today() >= end_date: - remaining_days = 0 - elif date.today() >= start_date: - remaining_days = (end_date - today).days - else: - remaining_days = (start_date - today).days - - attributes[ATTR_REMAINING_DAYS] = remaining_days - - last_indications_date = met.last_indications_date - attributes[ATTR_LAST_INDICATIONS_DATE] = ( - None if last_indications_date is None else last_indications_date.isoformat() - ) - - # Add zone information - for zone_id, zone_def in met.zones.items(): - iterator = [ - ("name", zone_def.name), - ("last_indication", zone_def.last_indication or 0.0), - ("today_indication", zone_def.today_indication), - ] - - if is_submittable: - submitted_indication = zone_def.today_indication - if submitted_indication is None and last_indications_date is not None: - # noinspection PyUnboundLocalVariable - if start_date <= last_indications_date <= end_date: - submitted_indication = zone_def.last_indication or 0.0 - iterator.append(("period_indication", submitted_indication)) - - for attribute, value in iterator: - attributes[f"zone_{zone_id}_{attribute}"] = value - - self._handle_dev_presentation( - attributes, - (), - ( - ATTR_METER_CODE, - ATTR_INSTALL_DATE, - ATTR_LAST_INDICATIONS_DATE, - *filter(lambda x: x.endswith("_indication"), attributes.keys()), - ), - ) - - return attributes - - @property - def name_format_values(self) -> Mapping[str, Any]: - meter = self._meter - return { - FORMAT_VAR_ID: meter.id or "", - FORMAT_VAR_TYPE_EN: "meter", - FORMAT_VAR_TYPE_RU: "счётчик", - } - - ################################################################################# - # Additional functionality - ################################################################################# - - def _fire_callback_event( - self, call_data: Mapping[str, Any], event_data: Mapping[str, Any], event_id: str, title: str - ): - meter = self._meter - hass = self.hass - comment = event_data.get(ATTR_COMMENT) - - if comment is not None: - message = str(comment) - comment = "Response comment: " + str(comment) - else: - comment = "Response comment not provided" - message = comment - - _LOGGER.log( - logging.INFO if event_data.get(ATTR_SUCCESS) else logging.ERROR, - RE_MULTI_SPACES.sub(" ", RE_HTML_TAGS.sub("", comment)), - ) - - meter_code = meter.code - - event_data = { - ATTR_ENTITY_ID: self.entity_id, - ATTR_METER_CODE: meter_code, - ATTR_CALL_PARAMS: dict(call_data), - ATTR_SUCCESS: False, - ATTR_INDICATIONS: None, - ATTR_COMMENT: None, - **event_data, - } - - _LOGGER.debug("Firing event '%s' with post_fields: %s" % (event_id, event_data)) - - hass.bus.async_fire(event_type=event_id, event_data=event_data) - - def _get_real_indications(self, call_data: Mapping) -> Mapping[str, Union[int, float]]: - indications: Mapping[str, Union[int, float]] = call_data[ATTR_INDICATIONS] - meter_zones = self._meter.zones - - for zone_id, new_value in indications.items(): - if zone_id not in meter_zones: - raise ValueError(f"meter zone {zone_id} does not exist") - - if call_data[ATTR_INCREMENTAL]: - return { - zone_id: ( - ( - meter_zones[zone_id].today_indication - or meter_zones[zone_id].last_indication - or 0 - ) - + new_value - ) - for zone_id, new_value in indications.items() - } - - return indications - - async def async_service_push_indications(self, **call_data): - """ - Push indications entity service. - :param call_data: Parameters for service call - :return: - """ - _LOGGER.info(self.log_prefix + "Begin handling indications submission") - - meter = self._meter - - if meter is None: - raise Exception("Meter is unavailable") - - meter_code = meter.code - - if not isinstance(meter, AbstractSubmittableMeter): - raise Exception("Meter '%s' does not support indications submission" % (meter_code,)) - - else: - event_data = {} - - try: - indications = self._get_real_indications(call_data) - - event_data[ATTR_INDICATIONS] = dict(indications) - - await with_auto_auth( - meter.account.api, - meter.async_submit_indications, - **indications, - ignore_periods=call_data[ATTR_IGNORE_PERIOD], - ignore_values=call_data[ATTR_IGNORE_INDICATIONS], - ) - - except EnergosbytException as e: - event_data[ATTR_COMMENT] = "API error: %s" % e - raise - - except BaseException as e: - event_data[ATTR_COMMENT] = "Unknown error: %r" % e - _LOGGER.error(event_data[ATTR_COMMENT]) - raise - - else: - event_data[ATTR_COMMENT] = "Indications submitted successfully" - event_data[ATTR_SUCCESS] = True - self.async_schedule_update_ha_state(force_refresh=True) - - finally: - self._fire_callback_event( - call_data, - event_data, - DOMAIN + "_" + SERVICE_PUSH_INDICATIONS, - "Передача показаний", - ) - - _LOGGER.info(self.log_prefix + "End handling indications submission") - - async def async_service_calculate_indications(self, **call_data): - meter = self._meter - - if meter is None: - raise Exception("Meter is unavailable") - - meter_code = meter.code - - _LOGGER.info(self.log_prefix + "Begin handling indications calculation") - - if not isinstance(meter, AbstractCalculatableMeter): - raise Exception("Meter '%s' does not support indications calculation" % (meter_code,)) - - event_data = {ATTR_CHARGED: None, ATTR_SUCCESS: False} - - try: - indications = self._get_real_indications(call_data) - - event_data[ATTR_INDICATIONS] = dict(indications) - - calculation = await with_auto_auth( - meter.account.api, - meter.async_calculate_indications, - **indications, - ignore_periods=call_data[ATTR_IGNORE_PERIOD], - ignore_values=call_data[ATTR_IGNORE_INDICATIONS], - ) - - except EnergosbytException as e: - event_data[ATTR_COMMENT] = "Error: %s" % e - raise - - except BaseException as e: - event_data[ATTR_COMMENT] = "Unknown error: %r" % e - _LOGGER.exception(event_data[ATTR_COMMENT]) - raise - - else: - event_data[ATTR_CHARGED] = float(calculation) - event_data[ATTR_COMMENT] = "Successful calculation" - event_data[ATTR_SUCCESS] = True - - self.async_schedule_update_ha_state(force_refresh=True) - - finally: - self._fire_callback_event( - call_data, - event_data, - DOMAIN + "_" + SERVICE_CALCULATE_INDICATIONS, - "Подсчёт показаний", - ) - - _LOGGER.info(self.log_prefix + "End handling indications calculation") - - -class LkcomuLastInvoice(LkcomuInterRAOEntity[AbstractAccountWithInvoices]): - config_key = CONF_LAST_INVOICE - - def __init__(self, *args, last_invoice: Optional["AbstractInvoice"] = None, **kwargs) -> None: - super().__init__(*args, **kwargs) - self._last_invoice = last_invoice - - self.entity_id: Optional[str] = "sensor." + slugify( - f"{self.account_provider_code or 'unknown'}_{self._account.code}_last_invoice" - ) - - @property - def code(self) -> str: - return self._account.code - - @property - def device_class(self) -> Optional[str]: - return DOMAIN + "_invoice" - - @property - def unique_id(self) -> str: - """Return the unique ID of the sensor""" - acc = self._account - return f"{acc.api.__class__.__name__}_lastinvoice_{acc.id}" - - @property - def state(self) -> Union[float, str]: - invoice = self._last_invoice - if invoice: - if self._account_config[CONF_DEV_PRESENTATION]: - return ("-" if (invoice.total or 0.0) < 0.0 else "") + "#####.###" - return round(invoice.total or 0.0, 2) - return STATE_UNKNOWN - - @property - def icon(self) -> str: - return "mdi:receipt" - - @property - def unit_of_measurement(self) -> str: - return "руб." if self._last_invoice else None - - @property - def sensor_related_attributes(self): - invoice = self._last_invoice - - if invoice: - attributes = invoice_to_attrs(invoice) - - self._handle_dev_presentation( - attributes, - (ATTR_PERIOD, ATTR_INVOICE_ID), - ( - ATTR_TOTAL, - ATTR_PAID, - ATTR_INITIAL, - ATTR_CHARGED, - ATTR_INSURANCE, - ATTR_BENEFITS, - ATTR_PENALTY, - ATTR_SERVICE, - ), - ) - - return attributes - - return {} - - @property - def name_format_values(self) -> Mapping[str, Any]: - invoice = self._last_invoice - return { - FORMAT_VAR_ID: invoice.id if invoice else "", - FORMAT_VAR_TYPE_EN: "last invoice", - FORMAT_VAR_TYPE_RU: "последняя квитанция", - } - - @classmethod - async def async_refresh_accounts( - cls, - entities: Dict[Hashable, _TLkcomuInterRAOEntity], - account: "Account", - config_entry: ConfigEntry, - account_config: ConfigType, - ): - entity_key = account.id - if isinstance(account, AbstractAccountWithInvoices): - try: - entity = entities[entity_key] - except KeyError: - entity = cls(account, account_config) - entities[entity_key] = entity - return [entity] - else: - if entity.enabled: - await entity.async_update_ha_state(force_refresh=True) - - return None - - async def async_update_internal(self) -> None: - self._last_invoice = await self._account.async_get_last_invoice() - - -async_setup_entry = make_common_async_setup_entry( - LkcomuAccount, - LkcomuLastInvoice, - LkcomuMeter, -) +""" +Sensor for Inter RAO cabinet. +Retrieves indications regarding current state of accounts. +""" +import logging +import re +from datetime import date, datetime +from enum import IntEnum +from typing import ( + Any, + ClassVar, + Dict, + Final, + Hashable, + Mapping, + Optional, + TypeVar, + Union, +) + +import voluptuous as vol +from homeassistant.components.sensor import SensorEntity, SensorDeviceClass +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_DESCRIPTION, + STATE_LOCKED, + STATE_OK, + STATE_PROBLEM, + STATE_UNKNOWN, +) +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.typing import ConfigType +from homeassistant.util import slugify + +from custom_components.lkcomu_interrao._base import ( + LkcomuInterRAOEntity, + SupportedServicesType, + make_common_async_setup_entry, +) +from custom_components.lkcomu_interrao._encoders import invoice_to_attrs, payment_to_attrs +from custom_components.lkcomu_interrao._util import with_auto_auth +from custom_components.lkcomu_interrao.const import ( + ATTR_ACCOUNT_CODE, + ATTR_ACCOUNT_ID, + ATTR_ADDRESS, + ATTR_CALL_PARAMS, + ATTR_CHARGED, + ATTR_COMMENT, + ATTR_DESCRIPTION, + ATTR_END, + ATTR_FULL_NAME, + ATTR_IGNORE_INDICATIONS, + ATTR_IGNORE_PERIOD, + ATTR_INCREMENTAL, + ATTR_INDICATIONS, + ATTR_INSTALL_DATE, + ATTR_LAST_INDICATIONS_DATE, + ATTR_LIVING_AREA, + ATTR_METER_CATEGORY, + ATTR_METER_CODE, + ATTR_METER_MODEL, + ATTR_MODEL, + ATTR_PREVIOUS, + ATTR_PROVIDER_NAME, + ATTR_PROVIDER_TYPE, + ATTR_REASON, + ATTR_REMAINING_DAYS, + ATTR_RESULT, + ATTR_SERVICE_NAME, + ATTR_SERVICE_TYPE, + ATTR_START, + ATTR_STATUS, + ATTR_SUBMIT_PERIOD_ACTIVE, + ATTR_SUBMIT_PERIOD_END, + ATTR_SUBMIT_PERIOD_START, + ATTR_SUCCESS, + ATTR_SUM, + ATTR_TOTAL_AREA, + CONF_ACCOUNTS, + CONF_LAST_INVOICE, + CONF_LOGOS, + CONF_METERS, + DATA_PROVIDER_LOGOS, + DOMAIN, + FORMAT_VAR_ID, + FORMAT_VAR_TYPE_EN, + FORMAT_VAR_TYPE_RU, +) +from inter_rao_energosbyt.exceptions import EnergosbytException +from inter_rao_energosbyt.interfaces import ( + AbstractAccountWithBalance, + AbstractAccountWithInvoices, + AbstractAccountWithMeters, + AbstractAccountWithPayments, + AbstractBalance, + AbstractCalculatableMeter, + AbstractInvoice, + AbstractMeter, + AbstractSubmittableMeter, + Account, +) +from inter_rao_energosbyt.presets.byt import AccountWithBytInfo, BytInfoSingle +from inter_rao_energosbyt.util import process_start_end_arguments + +_LOGGER = logging.getLogger(__name__) + +RE_HTML_TAGS = re.compile(r"<[^<]+?>") +RE_MULTI_SPACES = re.compile(r"\s{2,}") + +INDICATIONS_MAPPING_SCHEMA = vol.Schema( + { + vol.Required(vol.Match(r"t\d+")): cv.positive_float, + } +) + +INDICATIONS_SEQUENCE_SCHEMA = vol.All( + vol.Any(vol.All(cv.positive_float, cv.ensure_list), [cv.positive_float]), + lambda x: dict(map(lambda y: ("t" + str(y[0]), y[1]), enumerate(x, start=1))), +) + +CALCULATE_PUSH_INDICATIONS_SCHEMA = vol.All( + cv.make_entity_service_schema( + { + vol.Required(ATTR_INDICATIONS): vol.Any( + vol.All( + cv.string, + lambda x: list(map(str.strip, x.split(","))), + INDICATIONS_SEQUENCE_SCHEMA, + ), + INDICATIONS_MAPPING_SCHEMA, + INDICATIONS_SEQUENCE_SCHEMA, + ), + vol.Optional(ATTR_IGNORE_PERIOD, default=False): cv.boolean, + vol.Optional(ATTR_IGNORE_INDICATIONS, default=False): cv.boolean, + vol.Optional(ATTR_INCREMENTAL, default=False): cv.boolean, + } + ), +) + +SERVICE_PUSH_INDICATIONS: Final = "push_indications" +SERVICE_PUSH_INDICATIONS_SCHEMA: Final = CALCULATE_PUSH_INDICATIONS_SCHEMA + +SERVICE_CALCULATE_INDICATIONS: Final = "calculate_indications" +SERVICE_CALCULATE_INDICATIONS_SCHEMA: Final = CALCULATE_PUSH_INDICATIONS_SCHEMA + +_SERVICE_SCHEMA_BASE_DATED: Final = { + vol.Optional(ATTR_START, default=None): vol.Any(vol.Equal(None), cv.datetime), + vol.Optional(ATTR_END, default=None): vol.Any(vol.Equal(None), cv.datetime), +} + +FEATURE_PUSH_INDICATIONS: Final = 1 +FEATURE_CALCULATE_INDICATIONS: Final = FEATURE_PUSH_INDICATIONS * 2 +FEATURE_GET_PAYMENTS: Final = FEATURE_CALCULATE_INDICATIONS * 2 +FEATURE_GET_INVOICES: Final = FEATURE_GET_PAYMENTS * 2 + +SERVICE_SET_DESCRIPTION: Final = "set_description" +SERVICE_GET_PAYMENTS: Final = "get_payments" +SERVICE_GET_INVOICES: Final = "get_invoices" + +_TLkcomuInterRAOEntity = TypeVar("_TLkcomuInterRAOEntity", bound=LkcomuInterRAOEntity) + + +def get_supported_features(from_services: SupportedServicesType, for_object: Any) -> int: + features = 0 + for type_feature, services in from_services.items(): + if type_feature is None: + continue + check_cls, feature = type_feature + if isinstance(for_object, check_cls): + features |= feature + + return features + + +class LkcomuAccount(LkcomuInterRAOEntity[Account], SensorEntity): + """The class for this sensor""" + + _attr_icon = "mdi:lightning-bolt-circle" + _attr_unit_of_measurement = "руб." + _attr_device_class = SensorDeviceClass.MONETARY + + config_key: ClassVar[str] = CONF_ACCOUNTS + + _supported_services: ClassVar[SupportedServicesType] = { + None: { + "set_description": { + vol.Optional(CONF_DESCRIPTION): vol.Any(vol.Equal(None), cv.string), + }, + }, + (AbstractAccountWithInvoices, FEATURE_GET_INVOICES): { + "get_invoices": _SERVICE_SCHEMA_BASE_DATED, + }, + (AbstractAccountWithPayments, FEATURE_GET_PAYMENTS): { + "get_payments": _SERVICE_SCHEMA_BASE_DATED, + }, + } + + def __init__(self, *args, balance: Optional[AbstractBalance] = None, **kwargs) -> None: + super().__init__(*args, *kwargs) + self._balance = balance + + self.entity_id: Optional[str] = f"sensor." + slugify( + f"{self.account_provider_code or 'unknown'}_{self._account.code}_account" + ) + + @property + def entity_picture(self) -> Optional[str]: + if not self._account_config[CONF_LOGOS]: + return None + + logos = self.hass.data.get(DATA_PROVIDER_LOGOS) + if not logos: + return None + + account_provider_code = self.account_provider_code + if account_provider_code is None: + return None + + provider_logo = logos.get(account_provider_code) + if isinstance(provider_logo, str): + return provider_logo + + return None + + @property + def unique_id(self) -> str: + """Return the unique ID of the sensor""" + acc = self._account + return f"{acc.api.__class__.__name__}_account_{acc.id}" + + @property + def native_value(self) -> Union[str, float]: + if self._account.is_locked: + return STATE_PROBLEM + balance = self._balance + if balance is not None: + return round(balance.balance or 0.0, 2) # fixes -0.0 issues + return STATE_UNKNOWN + + @property + def sensor_related_attributes(self) -> Optional[Mapping[str, Any]]: + account = self._account + service_type_value = account.service_type + service_type = ( + service_type_value.name.lower() + if isinstance(service_type_value, IntEnum) + else str(service_type_value) + ) + + provider_type_value = account.provider_type + provider_type = ( + provider_type_value.name.lower() + if isinstance(provider_type_value, IntEnum) + else str(provider_type_value) + ) + + attributes = { + ATTR_ADDRESS: account.address, + ATTR_DESCRIPTION: account.description, + ATTR_PROVIDER_TYPE: provider_type, + ATTR_PROVIDER_NAME: account.provider_name, + ATTR_SERVICE_TYPE: service_type, + ATTR_SERVICE_NAME: account.service_name, + } + + if account.is_locked: + attributes[ATTR_STATUS] = STATE_LOCKED + attributes[ATTR_REASON] = account.lock_reason + + else: + attributes[ATTR_STATUS] = STATE_OK + + if isinstance(account, AccountWithBytInfo): + info = account.info + if info: + attributes.update( + { + ATTR_FULL_NAME: info.full_name, + ATTR_LIVING_AREA: info.living_area, + ATTR_TOTAL_AREA: info.total_area, + ATTR_METER_CATEGORY: info.meter_category, + ATTR_METER_CODE: info.meter_code, + } + ) + + zones = account.info.zones + if zones is not None: + for zone_id, zone_def in zones.items(): + attrs = ("name", "description", "tariff") + for prefix in ("", "within_"): + values = tuple(getattr(zone_def, prefix + attr) for attr in attrs) + if any(values): + attributes.update( + zip( + map(lambda x: f"zone_{zone_id}_{prefix}{x}", attrs), + values, + ) + ) + + if isinstance(info, BytInfoSingle): + attributes[ATTR_METER_MODEL] = info.meter_model + + return attributes + + @property + def name_format_values(self) -> Mapping[str, Any]: + """Return the name of the sensor""" + account = self._account + return { + FORMAT_VAR_ID: str(account.id), + FORMAT_VAR_TYPE_EN: "account", + FORMAT_VAR_TYPE_RU: "лицевой счёт", + } + + ################################################################################# + # Functional implementation of inherent class + ################################################################################# + + @classmethod + async def async_refresh_accounts( + cls, + entities: Dict[Hashable, _TLkcomuInterRAOEntity], + account: "Account", + config_entry: ConfigEntry, + account_config: ConfigType, + ): + entity_key = account.id + try: + entity = entities[entity_key] + except KeyError: + entity = cls(account, account_config) + entities[entity_key] = entity + + return [entity] + else: + if entity.enabled: + entity.async_schedule_update_ha_state(force_refresh=True) + + async def async_update_internal(self) -> None: + await self._account.async_update_related() + account = self._account + + if isinstance(account, AbstractAccountWithBalance): + self._balance = await account.async_get_balance() + + if isinstance(account, AccountWithBytInfo): + await account.async_update_info() + + self.register_supported_services(account) + + ################################################################################# + # Services callbacks + ################################################################################# + + @property + def supported_features(self) -> int: + return get_supported_features( + self._supported_services, + self._account, + ) + + async def async_service_get_payments(self, **call_data): + account = self._account + + _LOGGER.info(self.log_prefix + "Begin handling payments retrieval") + + if not isinstance(account, AbstractAccountWithPayments): + raise ValueError("account does not support payments retrieval") + + dt_start: Optional["datetime"] = call_data[ATTR_START] + dt_end: Optional["datetime"] = call_data[ATTR_END] + + dt_start, dt_end = process_start_end_arguments(dt_start, dt_end) + results = [] + + event_data = { + ATTR_ACCOUNT_CODE: account.code, + ATTR_ACCOUNT_ID: account.id, + ATTR_SUCCESS: False, + ATTR_START: dt_start.isoformat(), + ATTR_END: dt_end.isoformat(), + ATTR_RESULT: results, + ATTR_COMMENT: None, + ATTR_SUM: 0.0, + } + + try: + payments = await with_auto_auth( + account.api, + account.async_get_payments, + dt_start, + dt_end, + ) + + for payment in payments: + event_data[ATTR_SUM] += payment.amount + results.append(payment_to_attrs(payment)) + + except BaseException as e: + event_data[ATTR_COMMENT] = "Unknown error: %r" % e + _LOGGER.exception(event_data[ATTR_COMMENT]) + raise + else: + event_data[ATTR_SUCCESS] = True + + finally: + self.hass.bus.async_fire( + event_type=DOMAIN + "_" + SERVICE_GET_PAYMENTS, + event_data=event_data, + ) + + _LOGGER.info(self.log_prefix + "Finish handling payments retrieval") + + async def async_service_get_invoices(self, **call_data): + account = self._account + + _LOGGER.info(self.log_prefix + "Begin handling invoices retrieval") + + if not isinstance(account, AbstractAccountWithInvoices): + raise ValueError("account does not support invoices retrieval") + + dt_start: Optional["datetime"] = call_data[ATTR_START] + dt_end: Optional["datetime"] = call_data[ATTR_END] + + dt_start, dt_end = process_start_end_arguments(dt_start, dt_end) + results = [] + + event_data = { + ATTR_ACCOUNT_CODE: account.code, + ATTR_ACCOUNT_ID: account.id, + ATTR_SUCCESS: False, + ATTR_START: dt_start.isoformat(), + ATTR_END: dt_end.isoformat(), + ATTR_RESULT: results, + ATTR_COMMENT: None, + ATTR_SUM: 0.0, + } + + try: + invoices = await with_auto_auth( + account.api, + account.async_get_invoices, + dt_start, + dt_end, + ) + + for invoice in invoices: + event_data[ATTR_SUM] += invoice.total + results.append(invoice_to_attrs(invoice)) + + except BaseException as e: + event_data[ATTR_COMMENT] = "Unknown error: %r" % e + _LOGGER.exception(event_data[ATTR_COMMENT]) + raise + else: + event_data[ATTR_SUCCESS] = True + + finally: + self.hass.bus.async_fire( + event_type=DOMAIN + "_" + SERVICE_GET_INVOICES, + event_data=event_data, + ) + + _LOGGER.info(self.log_prefix + "Finish handling invoices retrieval") + + async def async_service_set_description(self, **call_data): + account = self._account + + _LOGGER.info(self.log_prefix + "Begin handling description setting") + + event_data = { + ATTR_ACCOUNT_CODE: account.code, + ATTR_ACCOUNT_ID: account.id, + ATTR_SUCCESS: False, + ATTR_DESCRIPTION: call_data.get(CONF_DESCRIPTION), + ATTR_PREVIOUS: account.description, + } + + try: + await with_auto_auth( + account.api, + account.async_set_description, + description=event_data[ATTR_DESCRIPTION], + update=False, + ) + + except EnergosbytException as e: + event_data[ATTR_COMMENT] = "Error: %s" % e + raise + + except Exception as e: + event_data[ATTR_COMMENT] = "Unknown error: %s" % e + _LOGGER.exception("Unknown error: %s", e) + raise + + else: + event_data[ATTR_COMMENT] = "Successful calculation" + event_data[ATTR_SUCCESS] = True + self.async_schedule_update_ha_state(force_refresh=True) + + finally: + self.hass.bus.async_fire( + event_type=DOMAIN + "_" + SERVICE_SET_DESCRIPTION, + event_data=event_data, + ) + + _LOGGER.info(self.log_prefix + "End handling indications calculation") + + +class LkcomuMeter(LkcomuInterRAOEntity[AbstractAccountWithMeters], SensorEntity): + """The class for this sensor""" + + _attr_icon = "mdi:counter" + + config_key: ClassVar[str] = CONF_METERS + + _supported_services: ClassVar[SupportedServicesType] = { + (AbstractSubmittableMeter, FEATURE_PUSH_INDICATIONS): { + "push_indications": SERVICE_PUSH_INDICATIONS_SCHEMA, + }, + (AbstractCalculatableMeter, FEATURE_CALCULATE_INDICATIONS): { + "calculate_indications": SERVICE_CALCULATE_INDICATIONS_SCHEMA, + }, + } + + def __init__(self, *args, meter: AbstractMeter, **kwargs) -> None: + super().__init__(*args, **kwargs) + self._meter = meter + + self.entity_id: Optional[str] = f"sensor." + slugify( + f"{self.account_provider_code or 'unknown'}_{self._account.code}_meter_{self.code}" + ) + + ################################################################################# + # Implementation base of inherent class + ################################################################################# + + @classmethod + async def async_refresh_accounts( + cls, + entities: Dict[Hashable, Optional[_TLkcomuInterRAOEntity]], + account: "Account", + config_entry: ConfigEntry, + account_config: ConfigType, + ): + new_meter_entities = [] + if isinstance(account, AbstractAccountWithMeters): + meters = await account.async_get_meters() + + for meter_id, meter in meters.items(): + entity_key = (account.id, meter_id) + try: + entity = entities[entity_key] + except KeyError: + entity = cls( + account, + account_config, + meter=meter, + ) + entities[entity_key] = entity + new_meter_entities.append(entity) + else: + if entity.enabled: + entity.async_schedule_update_ha_state(force_refresh=True) + + return new_meter_entities if new_meter_entities else None + + async def async_update_internal(self) -> None: + meters = await self._account.async_get_meters() + meter = meters.get(self._meter.id) + if meter is None: + self.hass.async_create_task(self.async_remove()) + else: + self.register_supported_services(meter) + + self._meter = meter + + ################################################################################# + # Data-oriented implementation of inherent class + ################################################################################# + + @property + def code(self) -> str: + return self._meter.code + + @property + def unique_id(self) -> str: + """Return the unique ID of the sensor""" + met = self._meter + acc = met.account + return f"{acc.api.__class__.__name__}_meter_{acc.id}_{met.id}" + + @property + def native_value(self) -> str: + return self._meter.status or STATE_OK + + @property + def supported_features(self) -> int: + meter = self._meter + return ( + isinstance(meter, AbstractSubmittableMeter) * FEATURE_PUSH_INDICATIONS + | isinstance(meter, AbstractCalculatableMeter) * FEATURE_CALCULATE_INDICATIONS + ) + + @property + def sensor_related_attributes(self) -> Optional[Mapping[str, Any]]: + met = self._meter + attributes = { + ATTR_METER_CODE: met.code, + ATTR_ACCOUNT_CODE: met.account.code, + } + + # Meter model attribute + model = met.model + if model: + attributes[ATTR_MODEL] = model + + # Installation date attribute + install_date = met.installation_date + if install_date: + attributes[ATTR_INSTALL_DATE] = install_date.isoformat() + + # Submission periods attributes + is_submittable = False + if isinstance(met, AbstractSubmittableMeter): + is_submittable = True # this weird hack calms my IDE + + # noinspection PyUnresolvedReferences + today = date.today() + start_date, end_date = met.submission_period + attributes[ATTR_SUBMIT_PERIOD_START] = start_date.isoformat() + attributes[ATTR_SUBMIT_PERIOD_END] = end_date.isoformat() + attributes[ATTR_SUBMIT_PERIOD_ACTIVE] = start_date <= today <= end_date + + if date.today() >= end_date: + remaining_days = 0 + elif date.today() >= start_date: + remaining_days = (end_date - today).days + else: + remaining_days = (start_date - today).days + + attributes[ATTR_REMAINING_DAYS] = remaining_days + + last_indications_date = met.last_indications_date + attributes[ATTR_LAST_INDICATIONS_DATE] = ( + None if last_indications_date is None else last_indications_date.isoformat() + ) + + # Add zone information + for zone_id, zone_def in met.zones.items(): + iterator = [ + ("name", zone_def.name), + ("last_indication", zone_def.last_indication or 0.0), + ("today_indication", zone_def.today_indication), + ] + + if is_submittable: + submitted_indication = zone_def.today_indication + if submitted_indication is None and last_indications_date is not None: + # noinspection PyUnboundLocalVariable + if start_date <= last_indications_date <= end_date: + submitted_indication = zone_def.last_indication or 0.0 + iterator.append(("period_indication", submitted_indication)) + + for attribute, value in iterator: + attributes[f"zone_{zone_id}_{attribute}"] = value + + return attributes + + @property + def name_format_values(self) -> Mapping[str, Any]: + meter = self._meter + return { + FORMAT_VAR_ID: meter.id or "", + FORMAT_VAR_TYPE_EN: "meter", + FORMAT_VAR_TYPE_RU: "счётчик", + } + + ################################################################################# + # Additional functionality + ################################################################################# + + def _fire_callback_event( + self, + call_data: Mapping[str, Any], + event_data: Mapping[str, Any], + event_id: str, + title: str, + ): + meter = self._meter + hass = self.hass + comment = event_data.get(ATTR_COMMENT) + + if comment is not None: + comment = "Response comment: " + str(comment) + else: + comment = "Response comment not provided" + + _LOGGER.log( + logging.INFO if event_data.get(ATTR_SUCCESS) else logging.ERROR, + RE_MULTI_SPACES.sub(" ", RE_HTML_TAGS.sub("", comment)), + ) + + meter_code = meter.code + + event_data = { + ATTR_ENTITY_ID: self.entity_id, + ATTR_METER_CODE: meter_code, + ATTR_CALL_PARAMS: dict(call_data), + ATTR_SUCCESS: False, + ATTR_INDICATIONS: None, + ATTR_COMMENT: None, + **event_data, + } + + _LOGGER.debug("Firing event '%s' with post_fields: %s" % (event_id, event_data)) + + hass.bus.async_fire(event_type=event_id, event_data=event_data) + + def _get_real_indications(self, call_data: Mapping) -> Mapping[str, Union[int, float]]: + indications: Mapping[str, Union[int, float]] = call_data[ATTR_INDICATIONS] + meter_zones = self._meter.zones + + for zone_id, new_value in indications.items(): + if zone_id not in meter_zones: + raise ValueError(f"meter zone {zone_id} does not exist") + + if call_data[ATTR_INCREMENTAL]: + return { + zone_id: ( + ( + meter_zones[zone_id].today_indication + or meter_zones[zone_id].last_indication + or 0 + ) + + new_value + ) + for zone_id, new_value in indications.items() + } + + return indications + + async def async_service_push_indications(self, **call_data): + """ + Push indications entity service. + :param call_data: Parameters for service call + :return: + """ + _LOGGER.info(self.log_prefix + "Begin handling indications submission") + + meter = self._meter + + if meter is None: + raise Exception("Meter is unavailable") + + meter_code = meter.code + + if not isinstance(meter, AbstractSubmittableMeter): + raise Exception("Meter '%s' does not support indications submission" % (meter_code,)) + + else: + event_data = {} + + try: + indications = self._get_real_indications(call_data) + + event_data[ATTR_INDICATIONS] = dict(indications) + + await with_auto_auth( + meter.account.api, + meter.async_submit_indications, + **indications, + ignore_periods=call_data[ATTR_IGNORE_PERIOD], + ignore_values=call_data[ATTR_IGNORE_INDICATIONS], + ) + + except EnergosbytException as e: + event_data[ATTR_COMMENT] = "API error: %s" % e + raise + + except BaseException as e: + event_data[ATTR_COMMENT] = "Unknown error: %r" % e + _LOGGER.error(event_data[ATTR_COMMENT]) + raise + + else: + event_data[ATTR_COMMENT] = "Indications submitted successfully" + event_data[ATTR_SUCCESS] = True + self.async_schedule_update_ha_state(force_refresh=True) + + finally: + self._fire_callback_event( + call_data, + event_data, + DOMAIN + "_" + SERVICE_PUSH_INDICATIONS, + "Передача показаний", + ) + + _LOGGER.info(self.log_prefix + "End handling indications submission") + + async def async_service_calculate_indications(self, **call_data): + meter = self._meter + + if meter is None: + raise Exception("Meter is unavailable") + + meter_code = meter.code + + _LOGGER.info(self.log_prefix + "Begin handling indications calculation") + + if not isinstance(meter, AbstractCalculatableMeter): + raise Exception("Meter '%s' does not support indications calculation" % (meter_code,)) + + event_data = {ATTR_CHARGED: None, ATTR_SUCCESS: False} + + try: + indications = self._get_real_indications(call_data) + + event_data[ATTR_INDICATIONS] = dict(indications) + + calculation = await with_auto_auth( + meter.account.api, + meter.async_calculate_indications, + **indications, + ignore_periods=call_data[ATTR_IGNORE_PERIOD], + ignore_values=call_data[ATTR_IGNORE_INDICATIONS], + ) + + except EnergosbytException as e: + event_data[ATTR_COMMENT] = "Error: %s" % e + raise + + except BaseException as e: + event_data[ATTR_COMMENT] = "Unknown error: %r" % e + _LOGGER.exception(event_data[ATTR_COMMENT]) + raise + + else: + event_data[ATTR_CHARGED] = float(calculation) + event_data[ATTR_COMMENT] = "Successful calculation" + event_data[ATTR_SUCCESS] = True + + self.async_schedule_update_ha_state(force_refresh=True) + + finally: + self._fire_callback_event( + call_data, + event_data, + DOMAIN + "_" + SERVICE_CALCULATE_INDICATIONS, + "Подсчёт показаний", + ) + + _LOGGER.info(self.log_prefix + "End handling indications calculation") + + +class LkcomuLastInvoice(LkcomuInterRAOEntity[AbstractAccountWithInvoices], SensorEntity): + _attr_unit_of_measurement = "руб." + _attr_icon = "mdi:receipt" + _attr_device_class = SensorDeviceClass.MONETARY + + config_key = CONF_LAST_INVOICE + + def __init__(self, *args, last_invoice: Optional["AbstractInvoice"] = None, **kwargs) -> None: + super().__init__(*args, **kwargs) + self._last_invoice = last_invoice + + self.entity_id: Optional[str] = "sensor." + slugify( + f"{self.account_provider_code or 'unknown'}_{self._account.code}_last_invoice" + ) + + @property + def unique_id(self) -> str: + """Return the unique ID of the sensor""" + acc = self._account + return f"{acc.api.__class__.__name__}_lastinvoice_{acc.id}" + + @property + def native_value(self) -> Union[float, str]: + invoice = self._last_invoice + if invoice: + return round(invoice.total or 0.0, 2) + return STATE_UNKNOWN + + @property + def sensor_related_attributes(self): + invoice = self._last_invoice + if invoice: + return invoice_to_attrs(invoice) + + @property + def name_format_values(self) -> Mapping[str, Any]: + invoice = self._last_invoice + return { + FORMAT_VAR_ID: invoice.id if invoice else "", + FORMAT_VAR_TYPE_EN: "last invoice", + FORMAT_VAR_TYPE_RU: "последняя квитанция", + } + + @classmethod + async def async_refresh_accounts( + cls, + entities: Dict[Hashable, _TLkcomuInterRAOEntity], + account: "Account", + config_entry: ConfigEntry, + account_config: ConfigType, + ): + if isinstance(account, AbstractAccountWithInvoices): + entity_key = account.id + + try: + entity = entities[entity_key] + except KeyError: + entity = cls(account, account_config) + entities[entity_key] = entity + return [entity] + else: + if entity.enabled: + await entity.async_update_ha_state(force_refresh=True) + + return None + + async def async_update_internal(self) -> None: + self._last_invoice = await self._account.async_get_last_invoice() + + +async_setup_entry = make_common_async_setup_entry( + LkcomuAccount, + LkcomuLastInvoice, + LkcomuMeter, +) diff --git a/custom_components/lkcomu_interrao/services.yaml b/custom_components/lkcomu_interrao/services.yaml index e096499..d28c46f 100644 --- a/custom_components/lkcomu_interrao/services.yaml +++ b/custom_components/lkcomu_interrao/services.yaml @@ -2,7 +2,8 @@ push_indications: description: 'Передать показания в личный кабинет' target: entity: - device_class: lkcomu_interrao_meter + integration: lkcomu_interrao + domain: sensor fields: indications: description: 'Список показаний (от 1 до 3) для тарифов: T1, T2, T3' @@ -41,7 +42,8 @@ calculate_indications: description: 'Подсчитать начисления по передаваемым показаниям' target: entity: - device_class: lkcomu_interrao_meter + integration: lkcomu_interrao + domain: sensor fields: indications: description: 'Список показаний (от 1 до 3) для тарифов: T1, T2, T3' @@ -81,7 +83,8 @@ set_description: description: "Задать комментарий к лицевому счёту. Пустой параметр `description` (или его упущение) очистит описание к лицевому счёту." target: entity: - device_class: lkcomu_interrao_account + integration: lkcomu_interrao + domain: sensor fields: description: description: 'Описание' @@ -95,7 +98,8 @@ get_payments: description: "Получить перечень платежей, связанных с лицевым счётом, которые находятся внутри заданного периода" target: entity: - device_class: lkcomu_interrao_account + integration: lkcomu_interrao + domain: sensor fields: start: description: "Дата начала периода" @@ -116,7 +120,8 @@ get_invoices: description: "Получить перечень квитанций, связанных с лицевым счётом, которые находятся внутри заданного периода" target: entity: - device_class: lkcomu_interrao_account + integration: lkcomu_interrao + domain: sensor fields: start: description: "Дата начала периода" diff --git a/hacs.json b/hacs.json index 59dbd27..e03089a 100644 --- a/hacs.json +++ b/hacs.json @@ -5,6 +5,6 @@ "render_readme": true, "domains": ["sensor", "binary_sensor"], "country": "ru", - "homeassistant": "2021.12.0", + "homeassistant": "2022.05.0", "iot_class": "Cloud Polling" } diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index eefd936..0000000 --- a/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -cached-property==1.5.1 -fake_useragent==0.1.11 -inter-rao-energosbyt-python==0.0.15 -homeassistant~=2021.6.4 -attr~=0.3.1 -attrs~=21.2.0 -voluptuous~=0.12.1 \ No newline at end of file