├── images ├── icon.png ├── logo.png ├── icon@2x.png ├── logo@2x.png ├── setup-01.png ├── setup-02.png ├── setup-03.png ├── setup-04.png ├── setup-05.png ├── setup-06.png ├── setup-07.png ├── setup-08.png ├── setup-09.png ├── buttons-01.png ├── notify-01.png ├── notify-02.png ├── sensors-01.png ├── sensors-02.png ├── sensors-03.png ├── sensors-04.png ├── services-01.png ├── services-02.png ├── services-03.png ├── services-04.png ├── telegram-01.png ├── telegram-02.png ├── automations-01.png ├── automations-02.png ├── automations-03.png ├── automations-05.png ├── telegram-01_01.png ├── get_gas-bill-date-lovelace.png ├── icon.svg └── logo.svg ├── hacs.json ├── .github └── workflows │ ├── hassfest.yaml │ └── validate.yml ├── automation ├── get_bill.yaml ├── get_bill_to_email.yaml ├── send_readings.yaml ├── notify_get_bill_to_email_completed.yaml ├── notify_send_readings_completed.yaml ├── notify_get_bill_completed.yaml └── notify_mygas_service_failed.yaml ├── custom_components └── mygas │ ├── exceptions.py │ ├── manifest.json │ ├── services.yaml │ ├── icons.json │ ├── __init__.py │ ├── const.py │ ├── translations │ ├── en.json │ └── ru.json │ ├── decorators.py │ ├── button.py │ ├── helpers.py │ ├── entity.py │ ├── config_flow.py │ ├── services.py │ ├── sensor.py │ └── coordinator.py ├── LICENSE ├── CHANGELOG.md ├── .gitignore └── README.md /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizardsystems/hass-mygas/HEAD/images/icon.png -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizardsystems/hass-mygas/HEAD/images/logo.png -------------------------------------------------------------------------------- /images/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizardsystems/hass-mygas/HEAD/images/icon@2x.png -------------------------------------------------------------------------------- /images/logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizardsystems/hass-mygas/HEAD/images/logo@2x.png -------------------------------------------------------------------------------- /images/setup-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizardsystems/hass-mygas/HEAD/images/setup-01.png -------------------------------------------------------------------------------- /images/setup-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizardsystems/hass-mygas/HEAD/images/setup-02.png -------------------------------------------------------------------------------- /images/setup-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizardsystems/hass-mygas/HEAD/images/setup-03.png -------------------------------------------------------------------------------- /images/setup-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizardsystems/hass-mygas/HEAD/images/setup-04.png -------------------------------------------------------------------------------- /images/setup-05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizardsystems/hass-mygas/HEAD/images/setup-05.png -------------------------------------------------------------------------------- /images/setup-06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizardsystems/hass-mygas/HEAD/images/setup-06.png -------------------------------------------------------------------------------- /images/setup-07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizardsystems/hass-mygas/HEAD/images/setup-07.png -------------------------------------------------------------------------------- /images/setup-08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizardsystems/hass-mygas/HEAD/images/setup-08.png -------------------------------------------------------------------------------- /images/setup-09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizardsystems/hass-mygas/HEAD/images/setup-09.png -------------------------------------------------------------------------------- /images/buttons-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizardsystems/hass-mygas/HEAD/images/buttons-01.png -------------------------------------------------------------------------------- /images/notify-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizardsystems/hass-mygas/HEAD/images/notify-01.png -------------------------------------------------------------------------------- /images/notify-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizardsystems/hass-mygas/HEAD/images/notify-02.png -------------------------------------------------------------------------------- /images/sensors-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizardsystems/hass-mygas/HEAD/images/sensors-01.png -------------------------------------------------------------------------------- /images/sensors-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizardsystems/hass-mygas/HEAD/images/sensors-02.png -------------------------------------------------------------------------------- /images/sensors-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizardsystems/hass-mygas/HEAD/images/sensors-03.png -------------------------------------------------------------------------------- /images/sensors-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizardsystems/hass-mygas/HEAD/images/sensors-04.png -------------------------------------------------------------------------------- /images/services-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizardsystems/hass-mygas/HEAD/images/services-01.png -------------------------------------------------------------------------------- /images/services-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizardsystems/hass-mygas/HEAD/images/services-02.png -------------------------------------------------------------------------------- /images/services-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizardsystems/hass-mygas/HEAD/images/services-03.png -------------------------------------------------------------------------------- /images/services-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizardsystems/hass-mygas/HEAD/images/services-04.png -------------------------------------------------------------------------------- /images/telegram-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizardsystems/hass-mygas/HEAD/images/telegram-01.png -------------------------------------------------------------------------------- /images/telegram-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizardsystems/hass-mygas/HEAD/images/telegram-02.png -------------------------------------------------------------------------------- /images/automations-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizardsystems/hass-mygas/HEAD/images/automations-01.png -------------------------------------------------------------------------------- /images/automations-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizardsystems/hass-mygas/HEAD/images/automations-02.png -------------------------------------------------------------------------------- /images/automations-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizardsystems/hass-mygas/HEAD/images/automations-03.png -------------------------------------------------------------------------------- /images/automations-05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizardsystems/hass-mygas/HEAD/images/automations-05.png -------------------------------------------------------------------------------- /images/telegram-01_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizardsystems/hass-mygas/HEAD/images/telegram-01_01.png -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MyGas Account integration", 3 | "render_readme": true, 4 | "country": "RU" 5 | } 6 | -------------------------------------------------------------------------------- /images/get_gas-bill-date-lovelace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizardsystems/hass-mygas/HEAD/images/get_gas-bill-date-lovelace.png -------------------------------------------------------------------------------- /.github/workflows/hassfest.yaml: -------------------------------------------------------------------------------- 1 | name: Validate with hassfest 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 0 * * *" 8 | 9 | jobs: 10 | validate: 11 | runs-on: "ubuntu-latest" 12 | steps: 13 | - uses: "actions/checkout@main" 14 | - uses: home-assistant/actions/hassfest@master -------------------------------------------------------------------------------- /automation/get_bill.yaml: -------------------------------------------------------------------------------- 1 | alias: "Мой Газ: Запросить счета за газ" 2 | description: "Запросить счета за газ в сервисе Мой Газ" 3 | trigger: 4 | - platform: time 5 | at: "01:00:00" 6 | condition: 7 | - condition: template 8 | value_template: "{{ now().day == 5 }}" 9 | action: 10 | - service: mygas.get_bill 11 | data: 12 | device_id: 13 | mode: single -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 0 * * *" 8 | workflow_dispatch: 9 | 10 | jobs: 11 | validate-hacs: 12 | runs-on: "ubuntu-latest" 13 | steps: 14 | - uses: "actions/checkout@main" 15 | - name: HACS validation 16 | uses: "hacs/action@main" 17 | with: 18 | category: "integration" -------------------------------------------------------------------------------- /automation/get_bill_to_email.yaml: -------------------------------------------------------------------------------- 1 | alias: "Мой Газ: Запросить счета за газ на электронную почту" 2 | description: "Запросить счета за газ на электронную почту в сервисе Мой Газ" 3 | trigger: 4 | - platform: time 5 | at: "01:00:00" 6 | condition: 7 | - condition: template 8 | value_template: "{{ now().day == 5 }}" 9 | action: 10 | - service: mygas.get_bill 11 | data: 12 | device_id: 13 | email: 14 | mode: single -------------------------------------------------------------------------------- /custom_components/mygas/exceptions.py: -------------------------------------------------------------------------------- 1 | """Exceptions for MyGas integration.""" 2 | from __future__ import annotations 3 | 4 | from homeassistant.exceptions import HomeAssistantError 5 | 6 | 7 | class CannotConnect(HomeAssistantError): 8 | """Error to indicate we cannot connect.""" 9 | 10 | 11 | class InvalidAuth(HomeAssistantError): 12 | """Error to indicate there is invalid auth.""" 13 | 14 | 15 | class NoDevicesError(HomeAssistantError): 16 | """Error to indicate there are no devices in account.""" 17 | -------------------------------------------------------------------------------- /custom_components/mygas/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "mygas", 3 | "name": "MyGas", 4 | "codeowners": [ 5 | "@lizardsystems" 6 | ], 7 | "after_dependencies": ["http"], 8 | "config_flow": true, 9 | "dependencies": [], 10 | "documentation": "https://github.com/lizardsystems/hass-mygas", 11 | "integration_type": "hub", 12 | "iot_class": "cloud_polling", 13 | "issue_tracker": "https://github.com/lizardsystems/hass-mygas/issues", 14 | "loggers": [ 15 | "aiomygas", 16 | "mygas" 17 | ], 18 | "requirements": [ 19 | "aiomygas==2.3.0" 20 | ], 21 | "ssdp": [], 22 | "version": "1.6.1", 23 | "zeroconf": [] 24 | } 25 | -------------------------------------------------------------------------------- /automation/send_readings.yaml: -------------------------------------------------------------------------------- 1 | alias: "Мой Газ: Отправить показания по газу" 2 | description: "Отправить показания по газу в сервис Мой Газ" 3 | trigger: 4 | - platform: time 5 | at: "02:00:00" 6 | condition: 7 | - condition: template 8 | value_template: "{{ now().day == 24 }}" 9 | action: 10 | - alias: "Мой Газ: Отправить показания (Дом)" 11 | service: mygas.send_readings 12 | data: 13 | value: 14 | device_id: 15 | - delay: 16 | hours: 0 17 | minutes: 10 18 | seconds: 0 19 | milliseconds: 0 20 | - service: mygas.refresh 21 | data: 22 | device_id: 23 | mode: single 24 | -------------------------------------------------------------------------------- /custom_components/mygas/services.yaml: -------------------------------------------------------------------------------- 1 | refresh: 2 | fields: 3 | device_id: 4 | required: true 5 | selector: 6 | device: 7 | filter: 8 | integration: mygas 9 | get_bill: 10 | fields: 11 | device_id: 12 | required: true 13 | selector: 14 | device: 15 | filter: 16 | integration: mygas 17 | date: 18 | required: false 19 | selector: 20 | date: 21 | email: 22 | required: false 23 | selector: 24 | text: 25 | 26 | send_readings: 27 | fields: 28 | device_id: 29 | required: true 30 | selector: 31 | device: 32 | filter: 33 | integration: mygas 34 | value: 35 | required: true 36 | selector: 37 | entity: 38 | filter: 39 | domain: sensor 40 | device_class: gas 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 LizardSystems 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /automation/notify_get_bill_to_email_completed.yaml: -------------------------------------------------------------------------------- 1 | alias: "Мой Газ: Уведомление о счете за газ на электронную почту" 2 | description: Уведомление о счете за газ на электронную почту от сервиса Мой Газ 3 | triggers: 4 | - event_type: mygas_get_bill_completed 5 | trigger: event 6 | conditions: 7 | - condition: template 8 | value_template: "{{ trigger.event.data.email != none }}" 9 | actions: 10 | - data: 11 | message: >- 12 | 🔥Газ. Счет для {{device_attr(trigger.event.data.device_id, 13 | 'name_by_user') or device_attr(trigger.event.data.device_id, 'name') }} 14 | за {{trigger.event.data.date}} 15 | config_entry_id: # Replace with your Telegram Bot config entry ID 16 | action: telegram_bot.send_message 17 | - data: 18 | message: >- 19 | 🔥Газ. Счет для {{device_attr(trigger.event.data.device_id, 20 | 'name_by_user') or device_attr(trigger.event.data.device_id, 'name') }} 21 | за {{trigger.event.data.date}} отправлен на 22 | {{trigger.event.data.email}}. 23 | action: notify.persistent_notification 24 | mode: single 25 | 26 | -------------------------------------------------------------------------------- /custom_components/mygas/icons.json: -------------------------------------------------------------------------------- 1 | { 2 | "entity": { 3 | "sensor": { 4 | "account": { 5 | "default": "mdi:identifier" 6 | }, 7 | "balance": { 8 | "default": "mdi:cash" 9 | }, 10 | "current_timestamp": { 11 | "default": "mdi:update" 12 | }, 13 | "counter": { 14 | "default": "mdi:meter-gas-outline" 15 | }, 16 | "average_rate": { 17 | "default": "mdi:gas-burner" 18 | }, 19 | "price": { 20 | "default": "mdi:cash" 21 | }, 22 | "readings_date": { 23 | "default": "mdi:calendar" 24 | }, 25 | "readings": { 26 | "default": "mdi:counter" 27 | }, 28 | "consumption": { 29 | "default": "mdi:speedometer" 30 | } 31 | }, 32 | "button": { 33 | "refresh": { 34 | "default": "mdi:refresh" 35 | }, 36 | "get_bill": { 37 | "default": "mdi:receipt-text-outline" 38 | } 39 | } 40 | }, 41 | "services": { 42 | "refresh": "mdi:refresh", 43 | "get_bill": "mdi:receipt-text-outline", 44 | "send_readings": "mdi:receipt-text-send-outline" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /automation/notify_send_readings_completed.yaml: -------------------------------------------------------------------------------- 1 | alias: "Мой Газ: Уведомление об отправленных показаниях" 2 | description: "Уведомление об отправленных показаниях в сервис Мой Газ" 3 | triggers: 4 | - event_type: mygas_send_readings_completed 5 | trigger: event 6 | conditions: [] 7 | actions: 8 | - data: 9 | config_entry_id: # Replace with your Telegram Bot config entry ID 10 | message: "Показания: {{ trigger.event.data.readings }}" 11 | title: >- 12 | 🔥Газ. Показания для {{ 13 | device_attr(trigger.event.data.device_id,'name_by_user') or 14 | device_attr(trigger.event.data.device_id, 'name') }} отправлены {{ 15 | now().strftime('%d-%m-%Y %H:%M') }} 16 | action: telegram_bot.send_message 17 | - data: 18 | title: >- 19 | 🔥Газ. Показания для {{ 20 | device_attr(trigger.event.data.device_id,'name_by_user') or 21 | device_attr(trigger.event.data.device_id, 'name') }} отправлены {{ 22 | now().strftime("%d-%m-%Y %H:%M") }} 23 | message: "Показания: {{ trigger.event.data.readings }}" 24 | action: notify.persistent_notification 25 | mode: single 26 | -------------------------------------------------------------------------------- /automation/notify_get_bill_completed.yaml: -------------------------------------------------------------------------------- 1 | alias: "Мой Газ: Уведомление о счете за газ" 2 | description: Уведомление о счете за газ от сервиса Мой Газ 3 | triggers: 4 | - event_type: mygas_get_bill_completed 5 | trigger: event 6 | conditions: [] 7 | actions: 8 | - data: 9 | url: "{{trigger.event.data.url}}" 10 | caption: >- 11 | 🔥Газ. Счет для {{device_attr(trigger.event.data.device_id, 12 | 'name_by_user') or device_attr(trigger.event.data.device_id, 'name') }} 13 | за {{trigger.event.data.date}} 14 | config_entry_id: # Replace with your Telegram Bot config entry ID 15 | action: telegram_bot.send_document 16 | - data: 17 | message: >- 18 | Скачать счет для [{{device_attr(trigger.event.data.device_id, 19 | 'name_by_user') or device_attr(trigger.event.data.device_id, 'name') 20 | }}]({{trigger.event.data.url}}) за {{trigger.event.data.date}}. 21 | title: >- 22 | 🔥Газ. Счет для {{device_attr(trigger.event.data.device_id, 23 | 'name_by_user') or device_attr(trigger.event.data.device_id, 'name') }} 24 | за {{trigger.event.data.date}} 25 | action: notify.persistent_notification 26 | mode: single 27 | -------------------------------------------------------------------------------- /custom_components/mygas/__init__.py: -------------------------------------------------------------------------------- 1 | """MyGas Account integration.""" 2 | from __future__ import annotations 3 | 4 | import logging 5 | 6 | from homeassistant.config_entries import ConfigEntry 7 | from homeassistant.core import HomeAssistant 8 | 9 | from .const import DOMAIN, PLATFORMS 10 | from .coordinator import MyGasCoordinator 11 | from .services import async_setup_services, async_unload_services 12 | 13 | _LOGGER = logging.getLogger(__name__) 14 | 15 | 16 | async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: 17 | """Set up MyGas from a config entry.""" 18 | 19 | # Set up the Coordinator 20 | _coordinator = MyGasCoordinator(hass, _LOGGER, config_entry=config_entry) 21 | 22 | # Sync with Coordinator 23 | await _coordinator.async_config_entry_first_refresh() 24 | 25 | # Store Entity and Initialize Platforms 26 | hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = _coordinator 27 | 28 | # Listen for option changes 29 | config_entry.async_on_unload(config_entry.add_update_listener(update_listener)) 30 | 31 | await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) 32 | 33 | await async_setup_services(hass) 34 | 35 | return True 36 | 37 | 38 | async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: 39 | """Unload a config entry.""" 40 | if unload_ok := await hass.config_entries.async_unload_platforms( 41 | config_entry, PLATFORMS 42 | ): 43 | hass.data[DOMAIN].pop(config_entry.entry_id) 44 | 45 | await async_unload_services(hass) 46 | 47 | return unload_ok 48 | 49 | 50 | async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: 51 | """Update listener.""" 52 | await hass.config_entries.async_reload(entry.entry_id) 53 | -------------------------------------------------------------------------------- /automation/notify_mygas_service_failed.yaml: -------------------------------------------------------------------------------- 1 | alias: "Мой Газ: Уведомление об ошибке при выполнения сервиса" 2 | description: Уведомление об ошибке при выполнения сервиса Мой Газ 3 | triggers: 4 | - event_type: mygas_send_readings_failed 5 | trigger: event 6 | - event_type: mygas_get_bill_failed 7 | trigger: event 8 | - event_type: mygas_refresh_failed 9 | trigger: event 10 | conditions: [] 11 | actions: 12 | - data: 13 | config_entry_id: # Replace with your Telegram Bot config entry ID 14 | message: "{{ trigger.event.data.error }}" 15 | title: >- 16 | 🔥Газ. {% if trigger.event.event_type == 'mygas_send_readings_failed' %} 17 | Ошибка при передаче показаний для {% elif trigger.event.event_type 18 | == 'mygas_get_bill_failed' %} Ошибка при получении счета для {% elif 19 | trigger.event.event_type == 'mygas_refresh_failed' %} Ошибка при 20 | обновлении информации для {% else %} Ошибка при выполнении сервиса для 21 | {% endif %} {{device_attr(trigger.event.data.device_id, 22 | 'name_by_user') or device_attr(trigger.event.data.device_id, 'name') 23 | }} от {{ now().strftime('%d-%m-%Y %H:%M')}} 24 | action: telegram_bot.send_message 25 | - data: 26 | title: >- 27 | 🔥Газ. {% if trigger.event.event_type == 'mygas_send_readings_failed' %} 28 | Ошибка при передаче показаний для {% elif trigger.event.event_type == 29 | 'mygas_get_bill_failed' %} Ошибка при получении счета для {% elif 30 | trigger.event.event_type == 'mygas_refresh_failed' %} Ошибка при 31 | обновлении информации для {% else %} Ошибка при выполнении сервиса для 32 | {% endif %} {{device_attr(trigger.event.data.device_id, 'name_by_user') 33 | or device_attr(trigger.event.data.device_id, 'name') }} от {{ 34 | now().strftime('%d-%m-%Y %H:%M')}} 35 | message: "{{ trigger.event.data.error }}" 36 | action: notify.persistent_notification 37 | mode: single 38 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.6.1] - 2025-11-23 9 | 10 | ### Fixed 11 | - Выводится предупреждение в лог если не найдены счетчики. 12 | 13 | ## [1.6] - 2025-11-22 14 | 15 | ### Fixed 16 | 17 | - Устаревший аргумент hass был убран из декоратора verify_domain_control. 18 | - Добавлена возможность реконфигурации интеграции без удаления и повторного добавления. 19 | - Обновлена возможность реавторизации интеграции при изменении учетных данных пользователя. 20 | - Улучшена типизация кода. 21 | - Добавлены yaml для автоматизации для нового Telegram bot (Home Assistant 2025.07). 22 | - Исправлены мелкие ошибки и улучшена стабильность интеграции. 23 | - Обновлена документация. 24 | 25 | ## [1.5.1] - 2025-05-08 26 | 27 | ### Fixed 28 | 29 | - Для запроса счета за прошедший месяц нужно указывать первый день текущего месяца, иначе будет прислан нулевой счет. 30 | 31 | 32 | ## [1.5.0] - 2025-04-23 33 | 34 | ### Added 35 | 36 | - Поддержка получения счетов на указанную дату (месяц) и за прошлый месяц (по умолчанию). 37 | 38 | ### Updated 39 | 40 | - Исправлена документация 41 | - Добавлен пример запроса счета на указанную дату 42 | - Добавлен пример запроса счета за текущий месяц 43 | 44 | ## [1.4.1] - 2025-03-21 45 | 46 | ### Changed 47 | 48 | - Изменена единица измерения на RUB/m³ 49 | 50 | 51 | ## [1.4.0] - 2025-01-08 52 | 53 | ### Updated 54 | 55 | - С января 2025 года при запросе счета необходимо указывать первый день текущего месяца, а не предыдущего месяца (для которого требуется расчет). 56 | 57 | 58 | ## [1.3.0] - 2024-12-11 59 | 60 | ### Added 61 | 62 | - Добавлено уведомление о запросе счета на email 63 | 64 | ### Fixed 65 | 66 | - При запросе счета на email через сервис get_bill возникала ошибка 67 | 68 | ## [1.2.0] - 2024-03-29 69 | 70 | ### Added 71 | 72 | - Добавлен файл icons.json 73 | 74 | 75 | ## [1.0.0] - 2024-02-19 76 | 77 | Первый публичный релиз. 78 | -------------------------------------------------------------------------------- /images/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /custom_components/mygas/const.py: -------------------------------------------------------------------------------- 1 | """Constants for the MyGas integration.""" 2 | 3 | from __future__ import annotations 4 | 5 | from datetime import timedelta 6 | from typing import Final 7 | 8 | from homeassistant.const import Platform 9 | 10 | DOMAIN: Final = "mygas" 11 | 12 | ATTRIBUTION: Final = "Данные получены от Мой Газ" 13 | MANUFACTURER: Final = "Мой Газ" 14 | MODEL: Final = "MyGas" 15 | 16 | API_TIMEOUT: Final = 30 17 | API_MAX_TRIES: Final = 3 18 | API_RETRY_DELAY: Final = 10 19 | UPDATE_HOUR_BEGIN: Final = 1 20 | UPDATE_HOUR_END: Final = 5 21 | UPDATE_INTERVAL: Final[timedelta] = timedelta(days=1) 22 | 23 | PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.SENSOR] 24 | 25 | REQUEST_REFRESH_DEFAULT_COOLDOWN = 5 26 | 27 | CONF_ACCOUNT: Final = "account" 28 | CONF_DATA: Final = "data" 29 | CONF_LINK: Final = "link" 30 | CONF_ACCOUNTS: Final = "accounts" 31 | CONF_RESULT: Final = "result" 32 | CONF_INFO: Final = "info" 33 | CONF_PAYMENT: Final = "payment" 34 | CONF_READINGS: Final = "readings" 35 | CONF_AUTO_UPDATE: Final = "auto_update" 36 | 37 | CONFIGURATION_URL: Final = "https://мойгаз.смородина.онлайн/" 38 | 39 | FORMAT_DATE_SHORT_YEAR: Final = "%d.%m.%y" 40 | FORMAT_DATE_FULL_YEAR: Final = "%d.%m.%Y" 41 | 42 | ATTR_LABEL: Final = "Label" 43 | 44 | REFRESH_TIMEOUT = timedelta(minutes=10) 45 | ATTR_VALUE: Final = "value" 46 | ATTR_STATUS: Final = "status" 47 | ATTR_EMAIL: Final = "email" 48 | ATTR_COUNTER: Final = "counter" 49 | ATTR_MESSAGE: Final = "message" 50 | ATTR_SENT: Final = "sent" 51 | ATTR_COORDINATOR: Final = "coordinator" 52 | ATTR_READINGS: Final = "readings" 53 | ATTR_BALANCE: Final = "balance" 54 | SERVICE_REFRESH: Final = "refresh" 55 | SERVICE_SEND_READINGS = "send_readings" 56 | SERVICE_GET_BILL: Final = "get_bill" 57 | ACTION_TYPE_SEND_READINGS: Final = "send_readings" 58 | ACTION_TYPE_BILL: Final = "get_bill" 59 | ACTION_TYPE_REFRESH: Final = "refresh" 60 | ATTR_LSPU_INFO: Final = "lspu_info" 61 | ATTR_ELS_INFO: Final = "els_info" 62 | ATTR_ELS = "els" 63 | ATTR_IS_ELS = "is_els" 64 | ATTR_LSPU_INFO_GROUP = "lspuInfoGroup" 65 | ATTR_COUNTERS: Final = "counters" 66 | ATTR_ALIAS: Final = "alias" 67 | ATTR_LAST_UPDATE_TIME: Final = "last_update_time" 68 | ATTR_JNT_ACCOUNT_NUM: Final = "jntAccountNum" 69 | ATTR_UUID: Final = "uuid" 70 | ATTR_SERIAL_NUM: Final = "serialNumber" 71 | ATTR_ACCOUNT_ID: Final = "accountId" 72 | -------------------------------------------------------------------------------- /custom_components/mygas/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "My Gas", 3 | "config": { 4 | "step": { 5 | "user": { 6 | "title": "Configure My Gas", 7 | "data": { 8 | "username": "Username", 9 | "password": "Password" 10 | } 11 | } 12 | }, 13 | "error": { 14 | "cannot_connect": "Failed to connect", 15 | "invalid_auth": "Invalid authentication", 16 | "unknown": "Unexpected error", 17 | "no_devices": "No devices found in account" 18 | }, 19 | "abort": { 20 | "already_configured": "Account is already configured", 21 | "reconfigure_successful": "Re-configuration was successful", 22 | "unique_id_mismatch": "Please ensure you reconfigure against the same account.", 23 | "reauth_successful": "Re-authentication was successful" 24 | } 25 | }, 26 | "entity": { 27 | "sensor": { 28 | "account": { 29 | "name": "Account" 30 | }, 31 | "balance": { 32 | "name": "Balance" 33 | }, 34 | "current_timestamp": { 35 | "name": "Last Data Update" 36 | }, 37 | "counter": { 38 | "name": "Meter" 39 | }, 40 | "average_rate": { 41 | "name": "Average Rate" 42 | }, 43 | "price": { 44 | "name": "Price" 45 | }, 46 | "readings_date": { 47 | "name": "Readings Date" 48 | }, 49 | "readings": { 50 | "name": "Readings" 51 | }, 52 | "consumption": { 53 | "name": "Consumption" 54 | } 55 | }, 56 | "button": { 57 | "refresh": { 58 | "name": "Refresh" 59 | }, 60 | "get_bill": { 61 | "name": "Get Bill" 62 | } 63 | } 64 | }, 65 | "services": { 66 | "refresh": { 67 | "name": "Refresh", 68 | "description": "Refresh data from account", 69 | "fields": { 70 | "device_id": { 71 | "name": "Device", 72 | "description": "Select My Gas metering device" 73 | } 74 | } 75 | }, 76 | "get_bill": { 77 | "name": "Get Bill", 78 | "description": "Get a bill from My Gas", 79 | "fields": { 80 | "device_id": { 81 | "name": "Device", 82 | "description": "Select My Gas metering device" 83 | }, 84 | "date": { 85 | "name": "Date", 86 | "description": "Date of the bill" 87 | }, 88 | "email": { 89 | "name": "Email", 90 | "description": "Send a bill to email" 91 | } 92 | } 93 | }, 94 | "send_readings": { 95 | "name": "Send Readings", 96 | "description": "Send readings to My Gas", 97 | "fields": { 98 | "device_id": { 99 | "name": "Device", 100 | "description": "Select My Gas metering device" 101 | }, 102 | "value": { 103 | "name": "Readings", 104 | "description": "Meter readings, m³" 105 | } 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /custom_components/mygas/decorators.py: -------------------------------------------------------------------------------- 1 | """MyGas decorators.""" 2 | 3 | from __future__ import annotations 4 | 5 | import asyncio 6 | from collections.abc import Awaitable, Callable, Coroutine 7 | from functools import wraps 8 | from random import randrange 9 | from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar 10 | 11 | from aiomygas.exceptions import MyGasApiError, MyGasAuthError 12 | 13 | from homeassistant.exceptions import ConfigEntryAuthFailed 14 | from homeassistant.helpers.update_coordinator import UpdateFailed 15 | 16 | from .const import API_MAX_TRIES, API_RETRY_DELAY, API_TIMEOUT 17 | 18 | if TYPE_CHECKING: 19 | from .coordinator import MyGasCoordinator 20 | 21 | _MyGasCoordinatorT = TypeVar("_MyGasCoordinatorT", bound="MyGasCoordinator") 22 | _R = TypeVar("_R") 23 | _P = ParamSpec("_P") 24 | 25 | 26 | def async_api_request_handler( 27 | method: Callable[Concatenate[_MyGasCoordinatorT, _P], Awaitable[_R]], 28 | ) -> Callable[Concatenate[_MyGasCoordinatorT, _P], Coroutine[Any, Any, _R]]: 29 | """Handle API errors.""" 30 | 31 | @wraps(method) 32 | async def wrapper( 33 | self: _MyGasCoordinatorT, *args: _P.args, **kwargs: _P.kwargs 34 | ) -> _R: 35 | """Wrap an API method.""" 36 | try: 37 | tries = 0 38 | api_timeout = API_TIMEOUT 39 | api_retry_delay = API_RETRY_DELAY 40 | while True: 41 | tries += 1 42 | try: 43 | async with asyncio.timeout(api_timeout): 44 | result = await method(self, *args, **kwargs) 45 | 46 | if result is not None: 47 | return result 48 | 49 | self.logger.error( 50 | "API error while execute function %s", method.__name__ 51 | ) 52 | raise MyGasApiError( 53 | f"API error while execute function {method.__name__}" 54 | ) 55 | 56 | except TimeoutError: 57 | api_timeout = tries * API_TIMEOUT 58 | self.logger.debug( 59 | "Function %s: Timeout connecting to MyGas", method.__name__ 60 | ) 61 | 62 | if tries >= API_MAX_TRIES: 63 | raise UpdateFailed( 64 | f"Invalid response from MyGas API: {method.__name__}" 65 | ) 66 | 67 | self.logger.warning( 68 | "Attempt %d/%d. Wait %d seconds and try again", 69 | tries, 70 | API_MAX_TRIES, 71 | api_retry_delay, 72 | ) 73 | await asyncio.sleep(api_retry_delay) 74 | api_retry_delay += API_RETRY_DELAY + randrange(API_RETRY_DELAY) 75 | 76 | except MyGasAuthError as exc: 77 | raise ConfigEntryAuthFailed("MyGas auth error") from exc 78 | except MyGasApiError as exc: 79 | raise UpdateFailed(f"Invalid response from MyGas API: {exc}") from exc 80 | 81 | return wrapper 82 | -------------------------------------------------------------------------------- /custom_components/mygas/translations/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Мой Газ", 3 | "config": { 4 | "step": { 5 | "user": { 6 | "title": "Укажите учетную запись сервиса Мой Газ", 7 | "data": { 8 | "password": "Пароль", 9 | "username": "Логин" 10 | } 11 | } 12 | }, 13 | "error": { 14 | "cannot_connect": "Не удалось подключиться.", 15 | "invalid_auth": "Ошибка аутентификации.", 16 | "unknown": "Непредвиденная ошибка.", 17 | "no_devices": "В аккаунте не найдено ни одного устройства." 18 | }, 19 | "abort": { 20 | "already_configured": "Этот аккаунт уже добавлен в Home Assistant.", 21 | "reconfigure_successful": "Повторная настройка выполнена успешно", 22 | "unique_id_mismatch": "Пожалуйста, убедитесь, что вы перенастраиваете тот же аккаунт.", 23 | "reauth_successful": "Повторная аутентификация выполнена успешно." 24 | } 25 | }, 26 | "entity": { 27 | "sensor": { 28 | "account": { 29 | "name": "Лицевой счет" 30 | }, 31 | "balance": { 32 | "name": "Задолженность" 33 | }, 34 | "current_timestamp": { 35 | "name": "Последнее обновление" 36 | }, 37 | "counter": { 38 | "name": "Счетчик" 39 | }, 40 | "average_rate": { 41 | "name": "Средний расход" 42 | }, 43 | "price": { 44 | "name": "Цена за м³" 45 | }, 46 | "readings_date": { 47 | "name": "Дата показаний" 48 | }, 49 | "readings": { 50 | "name": "Показания" 51 | }, 52 | "consumption": { 53 | "name": "Потребление" 54 | } 55 | }, 56 | "button": { 57 | "refresh": { 58 | "name": "Обновить" 59 | }, 60 | "get_bill": { 61 | "name": "Получить счет" 62 | } 63 | } 64 | }, 65 | "services": { 66 | "refresh": { 67 | "name": "Обновить", 68 | "description": "Обновить данные из личного кабинета", 69 | "fields": { 70 | "device_id": { 71 | "name": "Прибор учета", 72 | "description": "Выберите прибор учета" 73 | } 74 | } 75 | }, 76 | "get_bill": { 77 | "name": "Получить счет", 78 | "description": "Получить счет от Мой Газ", 79 | "fields": { 80 | "device_id": { 81 | "name": "Прибор учета", 82 | "description": "Выберите прибор учета" 83 | }, 84 | "date": { 85 | "name": "Дата", 86 | "description": "Дата счета" 87 | }, 88 | "email": { 89 | "name": "Электронная почта", 90 | "description": "Отправить счет на электронную почту" 91 | } 92 | } 93 | }, 94 | "send_readings": { 95 | "name": "Отправить показания", 96 | "description": "Отправить показания в Мой Газ", 97 | "fields": { 98 | "device_id": { 99 | "name": "Прибор учета", 100 | "description": "Выберите прибор учета" 101 | }, 102 | "value": { 103 | "name": "Показания", 104 | "description": "Показания счетчика, м³" 105 | } 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | .idea/ 161 | docs/ 162 | 163 | 164 | -------------------------------------------------------------------------------- /custom_components/mygas/button.py: -------------------------------------------------------------------------------- 1 | """Support for MyGas button.""" 2 | 3 | from __future__ import annotations 4 | 5 | from collections.abc import Awaitable, Callable 6 | from dataclasses import dataclass 7 | 8 | from homeassistant.components.button import ( 9 | ENTITY_ID_FORMAT, 10 | ButtonEntity, 11 | ButtonEntityDescription, 12 | ) 13 | from homeassistant.config_entries import ConfigEntry 14 | from homeassistant.const import ATTR_DEVICE_ID, ATTR_IDENTIFIERS 15 | from homeassistant.core import HomeAssistant 16 | from homeassistant.helpers.entity import EntityCategory, async_generate_entity_id 17 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 18 | from homeassistant.util import slugify 19 | 20 | from .const import DOMAIN, SERVICE_GET_BILL, SERVICE_REFRESH 21 | from .coordinator import MyGasCoordinator 22 | from .entity import MyGasBaseCoordinatorEntity 23 | 24 | 25 | @dataclass(kw_only=True, frozen=True) 26 | class MyGasButtonRequiredKeysMixin: 27 | """Mixin for required keys.""" 28 | 29 | async_press: Callable[[MyGasCoordinator, str], Awaitable] 30 | 31 | 32 | @dataclass(kw_only=True, frozen=True) 33 | class MyGasButtonEntityDescription( 34 | ButtonEntityDescription, MyGasButtonRequiredKeysMixin 35 | ): 36 | """Class describing MyGas button entities.""" 37 | 38 | 39 | BUTTON_DESCRIPTIONS: tuple[MyGasButtonEntityDescription, ...] = ( 40 | MyGasButtonEntityDescription( 41 | key="refresh", 42 | icon="mdi:refresh", 43 | name="Обновить сведения", 44 | entity_category=EntityCategory.DIAGNOSTIC, 45 | async_press=lambda coordinator, device_id: coordinator.hass.services.async_call( 46 | DOMAIN, SERVICE_REFRESH, {ATTR_DEVICE_ID: device_id}, blocking=True 47 | ), 48 | translation_key="refresh", 49 | ), 50 | MyGasButtonEntityDescription( 51 | key="get_bill", 52 | icon="mdi:receipt-text-outline", 53 | name="Получить счет", 54 | entity_category=EntityCategory.DIAGNOSTIC, 55 | async_press=lambda coordinator, device_id: coordinator.hass.services.async_call( 56 | DOMAIN, SERVICE_GET_BILL, {ATTR_DEVICE_ID: device_id}, blocking=True 57 | ), 58 | translation_key="get_bill", 59 | ), 60 | ) 61 | 62 | 63 | class MyGasButtonEntity(MyGasBaseCoordinatorEntity, ButtonEntity): 64 | """Representation of a MyGas button.""" 65 | 66 | entity_description: MyGasButtonEntityDescription 67 | 68 | def __init__( 69 | self, 70 | coordinator: MyGasCoordinator, 71 | entity_description: MyGasButtonEntityDescription, 72 | account_id: int, 73 | lspu_group_id: int, 74 | counter_id: int, 75 | ) -> None: 76 | """Initialize the Entity.""" 77 | super().__init__(coordinator, account_id, lspu_group_id, counter_id) 78 | self.entity_description = entity_description 79 | # Safely get identifiers from device_info (device_info may be None 80 | # and 'identifiers' is not a required TypedDict key) 81 | identifiers = (self.device_info or {}).get(ATTR_IDENTIFIERS, set()) 82 | first_identifier = next(iter(identifiers), ()) 83 | ids = [*list(first_identifier), entity_description.key] 84 | # Ensure all parts are strings before joining 85 | self._attr_unique_id = slugify("_".join(str(part) for part in ids)) 86 | self.entity_id = async_generate_entity_id( 87 | ENTITY_ID_FORMAT, self.unique_id, hass=coordinator.hass 88 | ) 89 | 90 | async def async_press(self) -> None: 91 | """Press the button.""" 92 | if not self.registry_entry: 93 | return 94 | if device_id := self.registry_entry.device_id: 95 | await self.entity_description.async_press(self.coordinator, device_id) 96 | 97 | 98 | async def async_setup_entry( 99 | hass: HomeAssistant, 100 | entry: ConfigEntry, 101 | async_add_entities: AddEntitiesCallback, 102 | ) -> None: 103 | """Set up a config entry.""" 104 | 105 | coordinator: MyGasCoordinator = hass.data[DOMAIN][entry.entry_id] 106 | entities: list[MyGasButtonEntity] = [] 107 | 108 | for account_id in coordinator.get_accounts(): 109 | for lspu_account_id in range(len(coordinator.get_lspu_accounts(account_id))): 110 | for counter_id in range( 111 | len(coordinator.get_counters(account_id, lspu_account_id)) 112 | ): 113 | entities.extend( 114 | MyGasButtonEntity( 115 | coordinator, 116 | entity_description, 117 | account_id, 118 | lspu_account_id, 119 | counter_id, 120 | ) 121 | for entity_description in BUTTON_DESCRIPTIONS 122 | ) 123 | 124 | async_add_entities(entities, True) 125 | -------------------------------------------------------------------------------- /images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /custom_components/mygas/helpers.py: -------------------------------------------------------------------------------- 1 | """MyGas helper function.""" 2 | 3 | from __future__ import annotations 4 | 5 | from datetime import date, datetime, timedelta 6 | from typing import TYPE_CHECKING, Any 7 | 8 | from homeassistant.core import HomeAssistant 9 | from homeassistant.helpers import device_registry as dr 10 | from homeassistant.util import dt as dt_util, slugify 11 | 12 | from .const import ATTR_COUNTER, DOMAIN 13 | 14 | if TYPE_CHECKING: 15 | from .coordinator import MyGasCoordinator 16 | 17 | 18 | async def async_get_device_entry_by_device_id( 19 | hass: HomeAssistant, device_id: str | None 20 | ) -> dr.DeviceEntry: 21 | """Get device entry by device id.""" 22 | if device_id is None: 23 | raise ValueError("Device is undefined") 24 | 25 | device_registry = dr.async_get(hass) 26 | device_entry = device_registry.async_get(device_id) 27 | if device_entry: 28 | return device_entry 29 | 30 | raise ValueError(f"Device {device_id} not found") 31 | 32 | 33 | async def async_get_device_friendly_name( 34 | hass: HomeAssistant, device_id: str | None 35 | ) -> str | None: 36 | """Get device friendly name.""" 37 | 38 | device_entry = await async_get_device_entry_by_device_id(hass, device_id) 39 | return device_entry.name_by_user or device_entry.name 40 | 41 | 42 | async def async_get_coordinator( 43 | hass: HomeAssistant, device_id: str | None 44 | ) -> MyGasCoordinator: 45 | """Get coordinator for device id.""" 46 | 47 | device_entry = await async_get_device_entry_by_device_id(hass, device_id) 48 | for entry_id in device_entry.config_entries: 49 | if (config_entry := hass.config_entries.async_get_entry(entry_id)) is None: 50 | continue 51 | if config_entry.domain == DOMAIN: 52 | return hass.data[DOMAIN][entry_id] 53 | 54 | raise ValueError(f"Config entry for {device_id} not found") 55 | 56 | 57 | def get_float_value(hass: HomeAssistant, entity_id: str | None) -> float | None: 58 | """Get float value from entity state.""" 59 | if entity_id is not None: 60 | cur_state = hass.states.get(entity_id) 61 | if cur_state is not None: 62 | return _to_float(cur_state.state) 63 | return None 64 | 65 | 66 | def get_int_value(hass: HomeAssistant, entity_id: str | None) -> float | None: 67 | """Get float value from entity state.""" 68 | if entity_id is not None: 69 | cur_state = hass.states.get(entity_id) 70 | if cur_state is not None: 71 | return _to_int(cur_state.state) 72 | return None 73 | 74 | 75 | def get_update_interval(hour: int, minute: int, second: int) -> timedelta: 76 | """Get update interval to time.""" 77 | now = dt_util.now() 78 | next_day = now + timedelta(days=1) 79 | next_time = next_day.replace(hour=hour, minute=minute, second=second) 80 | minutes_to_next_time = (next_time - now).total_seconds() / 60 81 | return timedelta(minutes=minutes_to_next_time) 82 | 83 | 84 | def get_bill_date() -> date: 85 | """Get first day of current month.""" 86 | today = date.today() 87 | return today.replace(day=1) # first day of current month 88 | 89 | 90 | def get_previous_month() -> date: 91 | """Get first day of previous month.""" 92 | today = date.today() 93 | return (today - timedelta(days=today.day)).replace( 94 | day=1 95 | ) # first day of the previous month 96 | 97 | 98 | def _to_str(value: Any) -> str | None: 99 | """Convert value to string.""" 100 | if value is None: 101 | return None 102 | try: 103 | s = str(value) 104 | except (TypeError, ValueError): 105 | return None 106 | 107 | return s 108 | 109 | 110 | def _to_bool(value: Any) -> bool | None: 111 | """Convert value to bool.""" 112 | if value is None: 113 | return None 114 | try: 115 | if isinstance(value, str): 116 | b = value.lower() == "true" 117 | else: 118 | b = bool(value) 119 | except (TypeError, ValueError): 120 | return None 121 | 122 | return b 123 | 124 | 125 | def _to_float(value: Any) -> float | None: 126 | """Convert value to float.""" 127 | if value is None: 128 | return None 129 | try: 130 | f = float(value) 131 | except (TypeError, ValueError): 132 | return None 133 | 134 | return f 135 | 136 | 137 | def _to_int(value: Any) -> int | None: 138 | """Convert value to int.""" 139 | if value is None: 140 | return None 141 | try: 142 | i = int(value) 143 | except (TypeError, ValueError): 144 | return None 145 | 146 | return i 147 | 148 | 149 | def _to_date(value: str | None, fmt: str) -> date | None: 150 | """Convert string value to date.""" 151 | if not value: 152 | return None 153 | try: 154 | d = datetime.strptime(value, fmt).date() 155 | except (TypeError, ValueError): 156 | return None 157 | 158 | return d 159 | 160 | 161 | def _to_year(value: str | None, fmt: str) -> int | None: 162 | """Convert string value to year.""" 163 | if value is None: 164 | return None 165 | try: 166 | y = datetime.strptime(value, fmt).year 167 | except (TypeError, ValueError): 168 | return None 169 | 170 | return y 171 | 172 | 173 | def make_device_id(account_number: str, counter_uuid: str) -> str: 174 | """Get device id.""" 175 | return slugify(f"{account_number}_{ATTR_COUNTER}_{counter_uuid}") 176 | -------------------------------------------------------------------------------- /custom_components/mygas/entity.py: -------------------------------------------------------------------------------- 1 | """Base entity for MyGas integration.""" 2 | 3 | from __future__ import annotations 4 | 5 | from collections.abc import Callable 6 | from dataclasses import dataclass 7 | from datetime import date, datetime 8 | from typing import Any 9 | 10 | from aiomygas.const import APP_VERSION, MOBILE_APP_NAME 11 | 12 | from homeassistant.components.sensor import SensorEntityDescription 13 | from homeassistant.const import ATTR_MODEL, ATTR_NAME 14 | from homeassistant.helpers.entity import DeviceInfo 15 | from homeassistant.helpers.typing import StateType 16 | from homeassistant.helpers.update_coordinator import CoordinatorEntity 17 | 18 | from .const import ( 19 | ATTR_SERIAL_NUM, 20 | ATTR_UUID, 21 | ATTRIBUTION, 22 | CONFIGURATION_URL, 23 | DOMAIN, 24 | MANUFACTURER, 25 | ) 26 | from .coordinator import MyGasCoordinator 27 | from .helpers import _to_date, _to_int, _to_str, make_device_id 28 | 29 | 30 | @dataclass(frozen=True, kw_only=True) 31 | class MyGasEntityDescriptionMixin: 32 | """Mixin for required MyGas base description keys.""" 33 | 34 | value_fn: Callable[[MyGasBaseCoordinatorEntity], StateType | datetime | date] 35 | 36 | 37 | @dataclass(frozen=True, kw_only=True) 38 | class MyGasBaseSensorEntityDescription(SensorEntityDescription): 39 | """Describes MyGas sensor entity default overrides.""" 40 | 41 | attr_fn: Callable[ 42 | [MyGasBaseCoordinatorEntity], dict[str, StateType | datetime | date] 43 | ] = lambda _: {} 44 | avabl_fn: Callable[[MyGasBaseCoordinatorEntity], bool] = lambda _: True 45 | icon_fn: Callable[[MyGasBaseCoordinatorEntity], str | None] = lambda _: None 46 | 47 | 48 | @dataclass(frozen=True, kw_only=True) 49 | class MyGasSensorEntityDescription( 50 | MyGasBaseSensorEntityDescription, MyGasEntityDescriptionMixin 51 | ): 52 | """Describes MyGas sensor entity.""" 53 | 54 | 55 | class MyGasBaseCoordinatorEntity(CoordinatorEntity[MyGasCoordinator]): 56 | """MyGas Base Entity.""" 57 | 58 | coordinator: MyGasCoordinator 59 | _attr_attribution = ATTRIBUTION 60 | _attr_has_entity_name = True 61 | account_id: int 62 | counter_id: int 63 | lspu_account_id: int 64 | 65 | def __init__( 66 | self, 67 | coordinator: MyGasCoordinator, 68 | account_id: int, 69 | lspu_account_id: int, 70 | counter_id: int, 71 | ) -> None: 72 | """Initialize the Entity.""" 73 | super().__init__(coordinator) 74 | self.account_id = account_id 75 | self.lspu_account_id = lspu_account_id 76 | self.counter_id = counter_id 77 | 78 | counter = coordinator.get_counters(self.account_id, self.lspu_account_id)[ 79 | counter_id 80 | ] 81 | 82 | device_id = make_device_id( 83 | coordinator.get_account_number(self.account_id, self.lspu_account_id), 84 | counter[ATTR_UUID], 85 | ) 86 | 87 | account_alias = coordinator.get_account_alias( 88 | self.account_id, self.lspu_account_id 89 | ) 90 | if account_alias: 91 | device_name = f"{counter[ATTR_NAME]} ({account_alias})" 92 | else: 93 | device_name = f"{counter[ATTR_NAME]}" 94 | 95 | self._attr_device_info = DeviceInfo( 96 | identifiers={(DOMAIN, device_id)}, 97 | manufacturer=MANUFACTURER, 98 | model=counter[ATTR_MODEL], 99 | name=device_name, 100 | serial_number=counter[ATTR_SERIAL_NUM], 101 | sw_version=APP_VERSION[MOBILE_APP_NAME], 102 | configuration_url=CONFIGURATION_URL, 103 | ) 104 | 105 | def get_lspu_account_data(self) -> dict[str | int, Any]: 106 | """Get LSPU account data.""" 107 | return self.coordinator.get_lspu_accounts(self.account_id)[self.lspu_account_id] 108 | 109 | def get_counter_data(self) -> dict[str, Any]: 110 | """Get counter data.""" 111 | return self.coordinator.get_counters(self.account_id, self.lspu_account_id)[ 112 | self.counter_id 113 | ] 114 | 115 | def get_latest_readings(self) -> dict[str, Any]: 116 | """Latest readings for counter.""" 117 | counter = self.coordinator.get_counters(self.account_id, self.lspu_account_id)[ 118 | self.counter_id 119 | ] 120 | values = counter.get("values", []) 121 | return values[0] if values else {} 122 | 123 | def get_counter_attr(self): 124 | """Get counter attr.""" 125 | counter = self.coordinator.get_counters(self.account_id, self.lspu_account_id)[ 126 | self.counter_id 127 | ] 128 | return { 129 | "Модель": _to_str(counter.get("model")), 130 | "Серийный номер": _to_str(counter.get("serialNumber")), 131 | "Состояние счетчика": _to_str(counter.get("state")), 132 | "Тип оборудования": _to_str(counter.get("equipmentKind")), 133 | "Расположение": _to_str(counter.get("position")), 134 | "Ресурс": _to_str(counter.get("serviceName")), 135 | "Тарифность": _to_int(counter.get("numberOfRates")), 136 | "Дата очередной поверки": _to_date( 137 | counter.get("checkDate"), "%Y-%m-%dT%H:%M:%S" 138 | ), 139 | "Плановая дата ТО": _to_date( 140 | counter.get("techSupportDate"), "%Y-%m-%dT%H:%M:%S" 141 | ), 142 | "Дата установки пломбы": _to_date( 143 | counter.get("sealDate"), "%Y-%m-%dT%H:%M:%S" 144 | ), 145 | "Дата заводской пломбы": _to_date( 146 | counter.get("factorySealDate"), "%Y-%m-%dT%H:%M:%S" 147 | ), 148 | "Дата изготовления прибора": _to_date( 149 | counter.get("commissionedOn"), "%Y-%m-%dT%H:%M:%S" 150 | ), 151 | } 152 | -------------------------------------------------------------------------------- /custom_components/mygas/config_flow.py: -------------------------------------------------------------------------------- 1 | """Config flow for MyGas integration.""" 2 | 3 | from __future__ import annotations 4 | 5 | import asyncio 6 | from collections.abc import Mapping 7 | import logging 8 | from random import randrange 9 | from typing import Any 10 | 11 | import aiohttp 12 | from aiomygas import MyGasApi, SimpleMyGasAuth 13 | from aiomygas.exceptions import MyGasApiError, MyGasAuthError 14 | import voluptuous as vol 15 | 16 | from homeassistant.config_entries import ConfigFlow, ConfigFlowResult 17 | from homeassistant.const import CONF_PASSWORD, CONF_USERNAME 18 | from homeassistant.core import HomeAssistant 19 | from homeassistant.helpers.aiohttp_client import async_get_clientsession 20 | 21 | from .const import API_MAX_TRIES, API_RETRY_DELAY, API_TIMEOUT, DOMAIN 22 | from .exceptions import CannotConnect, InvalidAuth 23 | 24 | _LOGGER = logging.getLogger(__name__) 25 | 26 | 27 | async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: 28 | """Validate the user input allows us to connect to MyGas.""" 29 | try: 30 | session = async_get_clientsession(hass) 31 | auth = SimpleMyGasAuth( 32 | identifier=data[CONF_USERNAME], 33 | password=data[CONF_PASSWORD], 34 | session=session, 35 | ) 36 | api = MyGasApi(auth) 37 | account = str(data[CONF_USERNAME]).lower() 38 | tries = 0 39 | api_timeout = API_TIMEOUT 40 | api_retry_delay = API_RETRY_DELAY 41 | _LOGGER.info("Connecting to MyGas account %s", account) 42 | while True: 43 | tries += 1 44 | try: 45 | async with asyncio.timeout(api_timeout): 46 | _ = await api.async_get_accounts() 47 | return {"title": str(data[CONF_USERNAME]).lower()} 48 | 49 | except TimeoutError: 50 | api_timeout += API_TIMEOUT 51 | _LOGGER.debug("Timeout connecting to MyGas account %s", account) 52 | 53 | if tries >= API_MAX_TRIES: 54 | raise CannotConnect 55 | 56 | # Wait before attempting to connect again. 57 | _LOGGER.warning( 58 | "Failed to connect to MyGas. Try %d: Wait %d seconds and try again", 59 | tries, 60 | api_retry_delay, 61 | ) 62 | await asyncio.sleep(api_retry_delay) 63 | api_retry_delay += API_RETRY_DELAY + randrange(API_RETRY_DELAY) 64 | 65 | except MyGasAuthError as exc: 66 | raise InvalidAuth from exc 67 | except (MyGasApiError, aiohttp.ClientError) as exc: 68 | raise CannotConnect from exc 69 | 70 | 71 | class MyGasConfigFlow(ConfigFlow, domain=DOMAIN): 72 | """Handle a config flow for MyGas.""" 73 | 74 | VERSION = 1 75 | 76 | async def async_step_user( 77 | self, user_input: dict[str, Any] | None = None 78 | ) -> ConfigFlowResult: 79 | """Handle the initial step.""" 80 | errors: dict[str, str] = {} 81 | if user_input is not None: 82 | await self.async_set_unique_id(f"{user_input[CONF_USERNAME].lower()}") 83 | self._abort_if_unique_id_configured() 84 | try: 85 | info = await validate_input(self.hass, user_input) 86 | except CannotConnect: 87 | errors["base"] = "cannot_connect" 88 | except InvalidAuth: 89 | errors["base"] = "invalid_auth" 90 | except Exception: # pylint: disable=broad-except 91 | _LOGGER.exception("Unexpected exception") 92 | errors["base"] = "unknown" 93 | else: 94 | return self.async_create_entry(title=info["title"], data=user_input) 95 | 96 | return self.async_show_form( 97 | step_id="user", 98 | data_schema=vol.Schema( 99 | { 100 | vol.Required(CONF_USERNAME): str, 101 | vol.Required(CONF_PASSWORD): str, 102 | } 103 | ), 104 | errors=errors, 105 | ) 106 | 107 | async def async_step_reconfigure(self, user_input: dict[str, Any] | None = None): 108 | """Handle reconfiguration of an existing MyGas config entry.""" 109 | errors: dict[str, str] = {} 110 | if user_input is not None: 111 | await self.async_set_unique_id(f"{user_input[CONF_USERNAME].lower()}") 112 | self._abort_if_unique_id_mismatch() 113 | return self.async_update_reload_and_abort( 114 | self._get_reconfigure_entry(), 115 | data_updates=user_input, 116 | ) 117 | reconf_entry = self._get_reconfigure_entry() 118 | 119 | return self.async_show_form( 120 | step_id="reconfigure", 121 | data_schema=vol.Schema( 122 | { 123 | vol.Required( 124 | CONF_USERNAME, default=reconf_entry.data[CONF_USERNAME] 125 | ): str, 126 | vol.Required( 127 | CONF_PASSWORD, default=reconf_entry.data[CONF_PASSWORD] 128 | ): str, 129 | } 130 | ), 131 | errors=errors, 132 | ) 133 | 134 | async def async_step_reauth( 135 | self, entry_data: Mapping[str, Any] 136 | ) -> ConfigFlowResult: 137 | """Handle reauthorization request from MyGas.""" 138 | return await self.async_step_reauth_confirm() 139 | 140 | async def async_step_reauth_confirm( 141 | self, user_input: dict[str, Any] | None = None 142 | ) -> ConfigFlowResult: 143 | """Confirm re-authentication with MyGas.""" 144 | errors: dict[str, str] = {} 145 | if user_input is not None: 146 | reauth_entry = self._get_reauth_entry() 147 | user_input = {**reauth_entry.data, **user_input} 148 | try: 149 | await validate_input(self.hass, user_input) 150 | except CannotConnect: 151 | errors["base"] = "cannot_connect" 152 | except InvalidAuth: 153 | errors["base"] = "invalid_auth" 154 | except Exception: # pylint: disable=broad-except 155 | _LOGGER.exception("Unexpected exception") 156 | errors["base"] = "unknown" 157 | else: 158 | return self.async_update_reload_and_abort(reauth_entry, data=user_input) 159 | 160 | return self.async_show_form( 161 | description_placeholders={CONF_USERNAME: reauth_entry.data[CONF_USERNAME]}, 162 | step_id="reauth_confirm", 163 | data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}), 164 | errors=errors, 165 | ) 166 | -------------------------------------------------------------------------------- /custom_components/mygas/services.py: -------------------------------------------------------------------------------- 1 | """MyGas services.""" 2 | 3 | from __future__ import annotations 4 | 5 | from collections.abc import Awaitable, Callable 6 | from dataclasses import dataclass 7 | import logging 8 | from math import ceil 9 | from typing import Any 10 | from urllib.parse import unquote 11 | 12 | import voluptuous as vol 13 | 14 | from homeassistant.const import ATTR_DATE, ATTR_DEVICE_ID, CONF_ERROR, CONF_URL 15 | from homeassistant.core import HomeAssistant, ServiceCall 16 | from homeassistant.exceptions import HomeAssistantError 17 | from homeassistant.helpers import config_validation as cv 18 | from homeassistant.helpers.service import verify_domain_control 19 | 20 | from .const import ( 21 | ATTR_COUNTERS, 22 | ATTR_EMAIL, 23 | ATTR_MESSAGE, 24 | ATTR_READINGS, 25 | ATTR_SENT, 26 | ATTR_VALUE, 27 | DOMAIN, 28 | SERVICE_GET_BILL, 29 | SERVICE_REFRESH, 30 | SERVICE_SEND_READINGS, 31 | ) 32 | from .coordinator import MyGasCoordinator 33 | from .helpers import async_get_coordinator, get_bill_date, get_float_value 34 | 35 | _LOGGER = logging.getLogger(__name__) 36 | 37 | SERVICE_BASE_SCHEMA = {vol.Required(ATTR_DEVICE_ID): cv.string} 38 | 39 | SERVICE_REFRESH_SCHEMA = vol.Schema( 40 | { 41 | **SERVICE_BASE_SCHEMA, 42 | } 43 | ) 44 | 45 | SERVICE_SEND_READINGS_SCHEMA = vol.Schema( 46 | vol.All( 47 | { 48 | **SERVICE_BASE_SCHEMA, 49 | vol.Required(ATTR_VALUE): cv.entity_id, 50 | } 51 | ), 52 | ) 53 | 54 | SERVICE_GET_BILL_SCHEMA = vol.Schema( 55 | { 56 | **SERVICE_BASE_SCHEMA, 57 | vol.Optional(ATTR_DATE): cv.date, 58 | vol.Optional(ATTR_EMAIL): vol.Email, 59 | }, 60 | ) 61 | 62 | 63 | @dataclass 64 | class ServiceDescription: 65 | """A class that describes MyGas services.""" 66 | 67 | name: str 68 | service_func: Callable[ 69 | [HomeAssistant, ServiceCall, MyGasCoordinator], Awaitable[dict[str, Any]] 70 | ] 71 | schema: vol.Schema | None = None 72 | 73 | 74 | async def _async_handle_refresh( 75 | hass: HomeAssistant, service_call: ServiceCall, coordinator: MyGasCoordinator 76 | ) -> dict[str, Any]: 77 | await coordinator.async_refresh() 78 | return {} 79 | 80 | 81 | async def _async_handle_send_readings( 82 | hass: HomeAssistant, service_call: ServiceCall, coordinator: MyGasCoordinator 83 | ) -> dict[str, Any]: 84 | value = get_float_value(hass, service_call.data.get(ATTR_VALUE)) 85 | if value is None: 86 | raise HomeAssistantError(f"{service_call.service}: Invalid reading value.") 87 | value = int(ceil(value)) # round to greater integer 88 | 89 | device_id = service_call.data.get(ATTR_DEVICE_ID) 90 | result = await coordinator.async_send_readings(device_id, value) 91 | 92 | if result is None: 93 | raise HomeAssistantError(f"{service_call.service}: Empty response from API.") 94 | 95 | if not isinstance(result, list) or len(result) == 0: 96 | raise HomeAssistantError( 97 | f"{service_call.service}: Unrecognised response from API: {result}" 98 | ) 99 | 100 | counters = result[0].get(ATTR_COUNTERS) 101 | 102 | if counters is None or not isinstance(counters, list) or len(counters) == 0: 103 | raise HomeAssistantError( 104 | f"{service_call.service}: Unrecognised response from API: {result}" 105 | ) 106 | 107 | counter = counters[0] # single counter for account 108 | 109 | message = counter.get(ATTR_MESSAGE) 110 | sent = counter.get(ATTR_SENT) 111 | if not sent: 112 | raise HomeAssistantError( 113 | f"{service_call.service}: Readings not sent: {message}" 114 | ) 115 | 116 | return {ATTR_READINGS: value, ATTR_SENT: sent, ATTR_MESSAGE: message} 117 | 118 | 119 | async def _async_handle_get_bill( 120 | hass: HomeAssistant, service_call: ServiceCall, coordinator: MyGasCoordinator 121 | ) -> dict[str, Any]: 122 | device_id = service_call.data.get(ATTR_DEVICE_ID) 123 | bill_date = service_call.data.get(ATTR_DATE, get_bill_date()) 124 | email = service_call.data.get(ATTR_EMAIL) 125 | if device_id is None: 126 | raise HomeAssistantError("Device is undefined") 127 | result = await coordinator.async_get_bill(device_id, bill_date, email) 128 | 129 | if result is None: 130 | raise HomeAssistantError(f"{service_call.service}: Empty response from API.") 131 | url = result.get(CONF_URL) 132 | if url is None and email is None: 133 | raise HomeAssistantError( 134 | f"{service_call.service}: Unrecognised response from API: {result}" 135 | ) 136 | return { 137 | ATTR_DATE: bill_date, 138 | CONF_URL: unquote(url) if url else None, 139 | ATTR_EMAIL: unquote(email) if email else None, 140 | } 141 | 142 | 143 | SERVICES: dict[str, ServiceDescription] = { 144 | SERVICE_REFRESH: ServiceDescription( 145 | SERVICE_REFRESH, _async_handle_refresh, SERVICE_REFRESH_SCHEMA 146 | ), 147 | SERVICE_SEND_READINGS: ServiceDescription( 148 | SERVICE_SEND_READINGS, _async_handle_send_readings, SERVICE_SEND_READINGS_SCHEMA 149 | ), 150 | SERVICE_GET_BILL: ServiceDescription( 151 | SERVICE_GET_BILL, _async_handle_get_bill, SERVICE_GET_BILL_SCHEMA 152 | ), 153 | } 154 | 155 | 156 | async def async_setup_services(hass: HomeAssistant) -> None: 157 | """Set up the MyGas services.""" 158 | 159 | @verify_domain_control(DOMAIN) 160 | async def _async_handle_service(service_call: ServiceCall) -> None: 161 | """Call a service.""" 162 | _LOGGER.debug("Service call %s", service_call.service) 163 | 164 | try: 165 | device_id = service_call.data.get(ATTR_DEVICE_ID) 166 | coordinator = await async_get_coordinator(hass, device_id) 167 | 168 | result = await SERVICES[service_call.service].service_func( 169 | hass, service_call, coordinator 170 | ) 171 | 172 | hass.bus.async_fire( 173 | event_type=f"{DOMAIN}_{service_call.service}_completed", 174 | event_data={ATTR_DEVICE_ID: device_id, **result}, 175 | context=service_call.context, 176 | ) 177 | 178 | _LOGGER.debug( 179 | "Service call '%s' successfully finished", service_call.service 180 | ) 181 | 182 | except Exception as exc: 183 | _LOGGER.error( 184 | "Service call '%s' failed. Error: %s", service_call.service, exc 185 | ) 186 | 187 | hass.bus.async_fire( 188 | event_type=f"{DOMAIN}_{service_call.service}_failed", 189 | event_data={ 190 | ATTR_DEVICE_ID: service_call.data.get(ATTR_DEVICE_ID), 191 | CONF_ERROR: str(exc), 192 | }, 193 | context=service_call.context, 194 | ) 195 | raise HomeAssistantError( 196 | f"Service call {service_call.service} failed. Error: {exc}" 197 | ) from exc 198 | 199 | for service in SERVICES.values(): 200 | if hass.services.has_service(DOMAIN, service.name): 201 | continue 202 | hass.services.async_register( 203 | DOMAIN, service.name, _async_handle_service, service.schema 204 | ) 205 | 206 | 207 | async def async_unload_services(hass: HomeAssistant) -> None: 208 | """Unload MyGas services.""" 209 | 210 | if hass.data.get(DOMAIN): 211 | return 212 | 213 | for service in SERVICES: 214 | hass.services.async_remove(domain=DOMAIN, service=service) 215 | -------------------------------------------------------------------------------- /custom_components/mygas/sensor.py: -------------------------------------------------------------------------------- 1 | """MyGas Sensor definitions.""" 2 | 3 | from __future__ import annotations 4 | 5 | from homeassistant.components.sensor import ( 6 | ENTITY_ID_FORMAT, 7 | SensorDeviceClass, 8 | SensorEntity, 9 | SensorStateClass, 10 | ) 11 | from homeassistant.config_entries import ConfigEntry 12 | from homeassistant.const import ATTR_IDENTIFIERS, UnitOfVolume 13 | from homeassistant.core import HomeAssistant, callback 14 | from homeassistant.helpers.entity import EntityCategory, async_generate_entity_id 15 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 16 | from homeassistant.util import slugify 17 | 18 | from .const import ATTR_LAST_UPDATE_TIME, DOMAIN 19 | from .coordinator import MyGasCoordinator 20 | from .entity import MyGasBaseCoordinatorEntity, MyGasSensorEntityDescription 21 | from .helpers import _to_date, _to_float, _to_str 22 | 23 | SENSOR_TYPES: tuple[MyGasSensorEntityDescription, ...] = ( 24 | # Информация по счету 25 | MyGasSensorEntityDescription( 26 | key="account", 27 | name="Лицевой счет", 28 | icon="mdi:identifier", 29 | entity_category=EntityCategory.DIAGNOSTIC, 30 | value_fn=lambda device: _to_str(device.get_lspu_account_data().get("account")), 31 | avabl_fn=lambda device: "account" in device.get_lspu_account_data(), 32 | translation_key="account", 33 | attr_fn=lambda device: { 34 | parameter["name"]: parameter["value"] 35 | for parameter in device.get_lspu_account_data().get("parameters", {}) 36 | }, 37 | ), 38 | MyGasSensorEntityDescription( 39 | key="balance", 40 | name="Задолженность", 41 | device_class=SensorDeviceClass.MONETARY, 42 | native_unit_of_measurement="RUB", 43 | value_fn=lambda device: _to_float( 44 | device.get_lspu_account_data().get("balance") 45 | ), 46 | avabl_fn=lambda device: "balance" in device.get_lspu_account_data(), 47 | translation_key="balance", 48 | ), 49 | MyGasSensorEntityDescription( 50 | key="current_timestamp", 51 | name="Последнее обновление", 52 | device_class=SensorDeviceClass.TIMESTAMP, 53 | value_fn=lambda device: device.coordinator.data.get(ATTR_LAST_UPDATE_TIME), 54 | avabl_fn=lambda device: ATTR_LAST_UPDATE_TIME in device.coordinator.data, 55 | entity_category=EntityCategory.DIAGNOSTIC, 56 | translation_key="current_timestamp", 57 | ), 58 | MyGasSensorEntityDescription( 59 | key="counter", 60 | name="Счетчик", 61 | icon="mdi:counter", 62 | value_fn=lambda device: device.get_counter_data().get("name"), 63 | avabl_fn=lambda device: "name" in device.get_counter_data(), 64 | translation_key="counter", 65 | entity_category=EntityCategory.DIAGNOSTIC, 66 | attr_fn=lambda device: device.get_counter_attr(), 67 | ), 68 | MyGasSensorEntityDescription( 69 | key="average_rate", 70 | name="Средний расход", 71 | native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, 72 | suggested_display_precision=1, 73 | device_class=SensorDeviceClass.GAS, 74 | # state_class=SensorStateClass.TOTAL, 75 | value_fn=lambda device: _to_float(device.get_counter_data().get("averageRate")), 76 | avabl_fn=lambda device: "averageRate" in device.get_counter_data(), 77 | translation_key="average_rate", 78 | ), 79 | MyGasSensorEntityDescription( 80 | key="price", 81 | name="Цена за м³", 82 | native_unit_of_measurement="RUB/m³", 83 | device_class=SensorDeviceClass.MONETARY, 84 | # state_class=SensorStateClass.TOTAL, 85 | value_fn=lambda device: _to_float( 86 | device.get_counter_data().get("price", {}).get("day") 87 | ), 88 | avabl_fn=lambda device: "price" in device.get_counter_data(), 89 | translation_key="price", 90 | ), 91 | MyGasSensorEntityDescription( 92 | key="readings_date", 93 | name="Дата показаний", 94 | device_class=SensorDeviceClass.DATE, 95 | value_fn=lambda device: _to_date( 96 | device.get_latest_readings().get("date"), "%Y-%m-%dT%H:%M:%S" 97 | ), 98 | avabl_fn=lambda device: "date" in device.get_latest_readings(), 99 | translation_key="readings_date", 100 | ), 101 | MyGasSensorEntityDescription( 102 | key="readings", 103 | name="Показания", 104 | native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, 105 | suggested_display_precision=1, 106 | device_class=SensorDeviceClass.GAS, 107 | state_class=SensorStateClass.TOTAL, 108 | value_fn=lambda device: _to_float(device.get_latest_readings().get("valueDay")), 109 | avabl_fn=lambda device: "valueDay" in device.get_latest_readings(), 110 | translation_key="readings", 111 | ), 112 | MyGasSensorEntityDescription( 113 | key="consumption", 114 | name="Потребление", 115 | native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, 116 | suggested_display_precision=1, 117 | device_class=SensorDeviceClass.GAS, 118 | state_class=SensorStateClass.TOTAL, 119 | value_fn=lambda device: _to_float(device.get_latest_readings().get("rate")), 120 | avabl_fn=lambda device: "rate" in device.get_latest_readings(), 121 | translation_key="consumption", 122 | ), 123 | ) 124 | 125 | 126 | class MyGasCounterCoordinatorEntity(MyGasBaseCoordinatorEntity, SensorEntity): 127 | """MyGas Counter Entity.""" 128 | 129 | entity_description: MyGasSensorEntityDescription 130 | 131 | def __init__( 132 | self, 133 | coordinator: MyGasCoordinator, 134 | entity_description: MyGasSensorEntityDescription, 135 | account_id: int, 136 | lspu_group_id: int, 137 | counter_id: int, 138 | ) -> None: 139 | """Initialize the Entity.""" 140 | super().__init__(coordinator, account_id, lspu_group_id, counter_id) 141 | self.entity_description = entity_description 142 | device_info = self.device_info 143 | assert device_info 144 | identifiers = device_info.get(ATTR_IDENTIFIERS) 145 | assert identifiers 146 | ids = [*list(next(iter(identifiers))), entity_description.key] 147 | self._attr_unique_id = slugify("_".join(ids)) 148 | 149 | self.entity_id = async_generate_entity_id( 150 | ENTITY_ID_FORMAT, self.unique_id, hass=coordinator.hass 151 | ) 152 | 153 | @property 154 | def available(self) -> bool: 155 | """Return True if sensor is available.""" 156 | return ( 157 | super().available 158 | and self.coordinator.data is not None 159 | and self.entity_description.avabl_fn(self) 160 | ) 161 | 162 | @callback 163 | def _handle_coordinator_update(self) -> None: 164 | """Handle updated data from the coordinator.""" 165 | self._attr_native_value = self.entity_description.value_fn(self) 166 | self._attr_extra_state_attributes = self.entity_description.attr_fn(self) 167 | if self.entity_description.icon_fn is not None: 168 | self._attr_icon = self.entity_description.icon_fn(self) 169 | 170 | self.coordinator.logger.debug( 171 | "Entity ID: %s Value: %s", self.entity_id, self.native_value 172 | ) 173 | 174 | super()._handle_coordinator_update() 175 | 176 | 177 | async def async_setup_entry( 178 | hass: HomeAssistant, 179 | entry: ConfigEntry, 180 | async_add_entities: AddEntitiesCallback, 181 | ) -> None: 182 | """Set up a config entry.""" 183 | 184 | coordinator: MyGasCoordinator = hass.data[DOMAIN][entry.entry_id] 185 | 186 | entities: list[MyGasCounterCoordinatorEntity] = [] 187 | for account_id in coordinator.get_accounts(): 188 | for lspu_account_id in range(len(coordinator.get_lspu_accounts(account_id))): 189 | for counter_id in range( 190 | len(coordinator.get_counters(account_id, lspu_account_id)) 191 | ): 192 | entities.extend( 193 | MyGasCounterCoordinatorEntity( 194 | coordinator, 195 | entity_description, 196 | account_id, 197 | lspu_account_id, 198 | counter_id, 199 | ) 200 | for entity_description in SENSOR_TYPES 201 | ) 202 | 203 | async_add_entities(entities, True) 204 | -------------------------------------------------------------------------------- /custom_components/mygas/coordinator.py: -------------------------------------------------------------------------------- 1 | """MyGas Account Coordinator.""" 2 | 3 | from __future__ import annotations 4 | 5 | from datetime import date 6 | import logging 7 | from random import randrange 8 | from typing import Any 9 | 10 | from aiomygas import MyGasApi, SimpleMyGasAuth 11 | from aiomygas.exceptions import MyGasAuthError 12 | 13 | from homeassistant.config_entries import ConfigEntry 14 | from homeassistant.const import CONF_PASSWORD, CONF_USERNAME 15 | from homeassistant.core import HomeAssistant 16 | from homeassistant.exceptions import ConfigEntryAuthFailed 17 | from homeassistant.helpers import device_registry as dr 18 | from homeassistant.helpers.aiohttp_client import async_get_clientsession 19 | from homeassistant.helpers.debounce import Debouncer 20 | from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed 21 | from homeassistant.util import dt as dt_util 22 | 23 | from .const import ( 24 | ATTR_ACCOUNT_ID, 25 | ATTR_ALIAS, 26 | ATTR_COUNTERS, 27 | ATTR_ELS, 28 | ATTR_IS_ELS, 29 | ATTR_JNT_ACCOUNT_NUM, 30 | ATTR_LAST_UPDATE_TIME, 31 | ATTR_LSPU_INFO_GROUP, 32 | ATTR_UUID, 33 | CONF_ACCOUNT, 34 | CONF_ACCOUNTS, 35 | CONF_AUTO_UPDATE, 36 | CONF_INFO, 37 | DOMAIN, 38 | REQUEST_REFRESH_DEFAULT_COOLDOWN, 39 | UPDATE_HOUR_BEGIN, 40 | UPDATE_HOUR_END, 41 | ) 42 | from .decorators import async_api_request_handler 43 | from .helpers import get_update_interval, make_device_id 44 | 45 | 46 | class MyGasCoordinator(DataUpdateCoordinator): 47 | """Coordinator is responsible for querying the device at a specified route.""" 48 | 49 | _api: MyGasApi 50 | data: dict[str, Any] 51 | username: str 52 | password: str 53 | force_next_update: bool 54 | auto_update: bool 55 | 56 | def __init__( 57 | self, 58 | hass: HomeAssistant, 59 | logger: logging.Logger, 60 | *, 61 | config_entry: ConfigEntry, 62 | ) -> None: 63 | """Initialise a custom coordinator.""" 64 | super().__init__( 65 | hass, 66 | logger, 67 | name=DOMAIN, 68 | request_refresh_debouncer=Debouncer( 69 | hass, 70 | logger, 71 | cooldown=REQUEST_REFRESH_DEFAULT_COOLDOWN, 72 | immediate=False, 73 | ), 74 | ) 75 | self.force_next_update = False 76 | self.data = {} 77 | session = async_get_clientsession(hass) 78 | self.username = config_entry.data[CONF_USERNAME] 79 | self.password = config_entry.data[CONF_PASSWORD] 80 | self.auto_update = config_entry.data.get(CONF_AUTO_UPDATE, False) 81 | auth = SimpleMyGasAuth(self.username, self.password, session) 82 | self._api = MyGasApi(auth) 83 | 84 | async def async_force_refresh(self): 85 | """Force refresh data.""" 86 | self.force_next_update = True 87 | await self.async_refresh() 88 | 89 | async def _async_update_data(self) -> dict[str, Any] | None: 90 | """Fetch data from MyGas.""" 91 | _data: dict[str, Any] = self.data if self.data is not None else {} 92 | new_data: dict[str, Any] = { 93 | ATTR_LAST_UPDATE_TIME: dt_util.now(), 94 | } 95 | self.logger.debug("Start updating data...") 96 | try: 97 | accounts_info = _data.get(CONF_ACCOUNTS) 98 | if accounts_info is None or self.force_next_update: 99 | # get account general information 100 | self.logger.debug("Get accounts info for %s", self.username) 101 | accounts_info = await self._async_get_accounts() 102 | if accounts_info: 103 | self.logger.debug( 104 | "Accounts info for %s retrieved successfully", self.username 105 | ) 106 | else: 107 | self.logger.warning( 108 | "Accounts info for %s not retrieved", self.username 109 | ) 110 | return new_data 111 | else: 112 | self.logger.debug( 113 | "Accounts info for %s retrieved from cache", self.username 114 | ) 115 | 116 | new_data[CONF_ACCOUNTS] = accounts_info 117 | 118 | if accounts_info.get("elsGroup"): 119 | self.logger.debug( 120 | "Accounts info for els accounts %s retrieved successfully", 121 | self.username, 122 | ) 123 | new_data[ATTR_IS_ELS] = True 124 | new_data[CONF_INFO] = await self.retrieve_els_accounts_info( 125 | accounts_info 126 | ) 127 | elif accounts_info.get("lspu"): 128 | self.logger.debug( 129 | "Accounts info for lspu accounts %s retrieved successfully", 130 | self.username, 131 | ) 132 | 133 | new_data[ATTR_IS_ELS] = False 134 | new_data[CONF_INFO] = await self.retrieve_lspu_accounts_info( 135 | accounts_info 136 | ) 137 | else: 138 | self.logger.warning( 139 | "Account %s does not have els or lspu in accounts info", 140 | self.username, 141 | ) 142 | return None 143 | 144 | except MyGasAuthError as exc: 145 | raise ConfigEntryAuthFailed("Incorrect Login or Password") from exc 146 | except Exception as exc: # pylint: disable=broad-except 147 | raise UpdateFailed(f"Error communicating with API: {exc}") from exc 148 | else: 149 | self.logger.debug("Data updated successfully for %s", self.username) 150 | self.logger.debug("%s", new_data) 151 | 152 | return new_data 153 | finally: 154 | self.force_next_update = False 155 | if self.auto_update: 156 | self.update_interval = get_update_interval( 157 | randrange(UPDATE_HOUR_BEGIN, UPDATE_HOUR_END), 158 | randrange(60), 159 | randrange(60), 160 | ) 161 | self.logger.debug( 162 | "Update interval: %s seconds", self.update_interval.total_seconds() 163 | ) 164 | else: 165 | self.update_interval = None 166 | 167 | async def retrieve_els_accounts_info(self, accounts_info): 168 | """Retrieve ELS accounts info.""" 169 | els_list = accounts_info.get("elsGroup") 170 | els_info = {} 171 | for els in els_list: 172 | els_id = els.get("els", {}).get("id") 173 | if not els_id: 174 | self.logger.warning("id not found in els info") 175 | continue 176 | els_id = int(els_id) 177 | self.logger.debug("Get els info for %d", els_id) 178 | els_item_info = await self._async_get_els_info(els_id) 179 | if els_item_info: 180 | els_info[els_id] = els_item_info 181 | self.logger.debug("Els info for id=%d retrieved successfully", els_id) 182 | else: 183 | self.logger.warning("Els info for id=%d not retrieved", els_id) 184 | return els_info 185 | 186 | async def retrieve_lspu_accounts_info(self, accounts_info): 187 | """Retrieve LSPU accounts info.""" 188 | lspu_list = accounts_info.get("lspu") 189 | lspu_info = {} 190 | for lspu in lspu_list: 191 | lspu_id = lspu.get("id") 192 | if not lspu_id: 193 | self.logger.warning("id not found in lspu info") 194 | continue 195 | 196 | lspu_id = int(lspu_id) 197 | self.logger.debug("Get lspu info for %s", lspu_id) 198 | lspu_item_info = await self._async_get_lspu_info(lspu_id) 199 | if lspu_item_info: 200 | if lspu_item_info is list: 201 | lspu_info[lspu_id] = lspu_item_info 202 | else: 203 | lspu_info[lspu_id] = [lspu_item_info] 204 | self.logger.debug("Lspu info for %s retrieved successfully", lspu_id) 205 | else: 206 | self.logger.warning("Lspu info for %s not retrieved", lspu_id) 207 | return lspu_info 208 | 209 | def get_accounts(self) -> dict[int, dict[str | int, Any]]: 210 | """Get accounts info.""" 211 | return self.data.get(CONF_INFO, {}) 212 | 213 | def get_account_number(self, account_id: int, lspu_account_id: int) -> str: 214 | """Get account number.""" 215 | account = self.get_accounts()[account_id] 216 | if self.is_els(): 217 | _account_number = account.get(ATTR_ELS, {}).get(ATTR_JNT_ACCOUNT_NUM) 218 | else: 219 | _account_number = account[lspu_account_id].get(CONF_ACCOUNT) 220 | return _account_number 221 | 222 | def get_account_alias(self, account_id: int, lspu_account_id: int) -> str | None: 223 | """Get account alias.""" 224 | account = self.get_accounts()[account_id] 225 | if self.is_els(): 226 | _account_alias = account.get(ATTR_ELS, {}).get(ATTR_ALIAS) 227 | else: 228 | _account_alias = account[lspu_account_id].get(ATTR_ALIAS) 229 | return _account_alias 230 | 231 | def is_els(self) -> bool: 232 | """Account is ELS.""" 233 | return self.data.get(ATTR_IS_ELS, False) 234 | 235 | def get_lspu_accounts(self, account_id: int) -> list[dict[str | int, Any]]: 236 | """Get LSPU accounts.""" 237 | _data = self.get_accounts()[account_id] 238 | if self.is_els(): 239 | _lspu_accounts = _data[ATTR_LSPU_INFO_GROUP] 240 | else: 241 | _lspu_accounts = _data if isinstance(_data, list) else [_data] 242 | return _lspu_accounts 243 | 244 | def get_counters( 245 | self, account_id: int, lspu_acount_id: int 246 | ) -> list[dict[str, Any]]: 247 | """Get counter data.""" 248 | _accounts = self.get_lspu_accounts(account_id)[lspu_acount_id] 249 | counters = _accounts.get(ATTR_COUNTERS, []) 250 | if not counters: 251 | self.logger.warning( 252 | "No counters found for account_id=%d lspu_account_id=%d", 253 | account_id, 254 | lspu_acount_id, 255 | ) 256 | return counters 257 | 258 | async def find_account_by_device_id( 259 | self, device_id: str 260 | ) -> tuple[int | None, int | None, int | None] | None: 261 | """Find device by id.""" 262 | device_registry = dr.async_get(self.hass) 263 | device = device_registry.async_get(device_id) 264 | assert device 265 | 266 | for account_id in self.get_accounts(): 267 | for lspu_account_id in range(len(self.get_lspu_accounts(account_id))): 268 | for counter_id in range( 269 | len(self.get_counters(account_id, lspu_account_id)) 270 | ): 271 | _account_number = self.get_account_number( 272 | account_id, lspu_account_id 273 | ) 274 | _counter_uuid = self.get_counters(account_id, lspu_account_id)[ 275 | counter_id 276 | ].get(ATTR_UUID) 277 | assert _counter_uuid 278 | if device.identifiers == { 279 | (DOMAIN, make_device_id(_account_number, _counter_uuid)) 280 | }: 281 | return account_id, lspu_account_id, counter_id 282 | return None, None, None 283 | 284 | @async_api_request_handler 285 | async def _async_get_client_info(self) -> dict[str, Any]: 286 | """Fetch client info.""" 287 | return await self._api.async_get_client_info() 288 | 289 | @async_api_request_handler 290 | async def _async_get_accounts(self) -> dict[str, Any]: 291 | """Fetch accounts info.""" 292 | return await self._api.async_get_accounts() 293 | 294 | @async_api_request_handler 295 | async def _async_get_els_info(self, els_id: int) -> dict[str, Any]: 296 | """Fetch els info.""" 297 | return await self._api.async_get_els_info(els_id) 298 | 299 | @async_api_request_handler 300 | async def _async_get_lspu_info(self, lspu_id: int) -> dict[str, Any]: 301 | """Fetch lspu info.""" 302 | return await self._api.async_get_lspu_info(lspu_id) 303 | 304 | @async_api_request_handler 305 | async def _async_get_charges(self, lspu_id: int) -> dict[str, Any]: 306 | """Fetch charges info.""" 307 | return await self._api.async_get_charges(lspu_id) 308 | 309 | @async_api_request_handler 310 | async def _async_get_payments(self, lspu_id: int) -> dict[str, Any]: 311 | """Fetch payments info.""" 312 | return await self._api.async_get_payments(lspu_id) 313 | 314 | @async_api_request_handler 315 | async def _async_send_readings( 316 | self, 317 | lspu_id: int, 318 | equipment_uuid: str, 319 | value: float, 320 | els_id: int | None = None, 321 | ) -> list[dict[str, Any]]: 322 | """Send readings with handle errors by decorator.""" 323 | return await self._api.async_indication_send( 324 | lspu_id, equipment_uuid, value, els_id 325 | ) 326 | 327 | @async_api_request_handler 328 | async def _async_get_receipt( 329 | self, date_iso_short: str, email: str | None, account_number: int, is_els: bool 330 | ) -> dict[str, Any]: 331 | """Get receipt data.""" 332 | return await self._api.async_get_receipt( 333 | date_iso_short, 334 | email, # pyright: ignore[reportArgumentType] 335 | account_number, 336 | is_els, 337 | ) 338 | 339 | async def async_get_bill( 340 | self, 341 | device_id: str, 342 | bill_date: date | None = None, 343 | email: str | None = None, 344 | ) -> dict[str, Any] | None: 345 | """Get receipt data.""" 346 | if bill_date is None: 347 | bill_date = date.today() 348 | date_iso_short = bill_date.strftime("%Y-%m-%d") 349 | device = await self.find_account_by_device_id(device_id) 350 | assert device 351 | account_id, *_ = device 352 | if account_id is not None: 353 | is_els = self.is_els() 354 | return await self._async_get_receipt( 355 | date_iso_short, email, account_id, is_els 356 | ) 357 | return None 358 | 359 | async def async_send_readings( 360 | self, 361 | device_id, 362 | value: float, 363 | ) -> list[dict[str, Any]]: 364 | """Send readings with handle errors by decorator.""" 365 | device = await self.find_account_by_device_id(device_id) 366 | assert device 367 | account_id, lspu_account_id, counter_id = device 368 | assert account_id is not None 369 | assert counter_id is not None 370 | assert lspu_account_id is not None 371 | lspu_accounts = self.get_lspu_accounts(account_id) 372 | assert lspu_accounts 373 | lspu_account = lspu_accounts[lspu_account_id] 374 | assert lspu_account 375 | lspu_id = lspu_account[ATTR_ACCOUNT_ID] 376 | 377 | if self.is_els(): 378 | els_id = account_id 379 | else: 380 | els_id = None 381 | counters = self.get_counters(account_id, lspu_account_id) 382 | assert counters 383 | equipment_uuid = counters[counter_id][ATTR_UUID] 384 | assert equipment_uuid 385 | return await self._async_send_readings( 386 | lspu_id, 387 | equipment_uuid, 388 | value, 389 | els_id, 390 | ) 391 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![hacs_badge](https://img.shields.io/badge/HACS-Custom-41BDF5.svg?style=for-the-badge)](https://github.com/hacs/integration) 2 | 3 | ![my_gas 1](images/logo.png) 4 | 5 | Этот репозиторий содержит настраиваемый компонент для Home Assistant для отображения данных из сервиса Мой Газ Смородина. 6 | 7 | **Важно!** Данная интеграция не поддерживает аккаунты пользователей, у которых нет подключенных приборов учета газа (счетчиков). 8 | 9 | # Установка 10 | 11 | **Способ 1.** [![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=lizardsystems&repository=hass-mygas&category=integration) 12 | → Установить 13 | 14 | **Способ 2.** Вручную скопируйте папку `mygas` 15 | из [latest release](https://github.com/lizardsystems/hass-mygas/releases/latest) в 16 | директорию `/config/custom_components`. 17 | 18 | После установки необходимо перегрузить Home Assistant 19 | 20 | # Настройка 21 | 22 | [Настройки](https://my.home-assistant.io/redirect/config) → Устройства и службы 23 | → [Интеграции](https://my.home-assistant.io/redirect/integrations) 24 | → [Добавить интеграцию](https://my.home-assistant.io/redirect/config_flow_start?domain=mygas) → Поиск **MyGas** 25 | 26 | или нажмите: 27 | 28 | [![Добавить интеграцию](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start?domain=mygas) 29 | 30 | ![Установка mygas 1](images/setup-01.png) 31 | 32 | Появится окно настройки интеграции, укажите в нем логин и пароль для доступа в личный кабинет в 33 | сервисе [Мой Газ](https://мойгаз.смородина.онлайн/). 34 | 35 | ![Установка mygas 2](images/setup-02.png) 36 | 37 | Если вы ввели логин и пароль правильно, то появится сообщение об успешном окончании настройки. 38 | 39 | ![Установка mygas 3](images/setup-03.png) 40 | 41 | После подключения интеграции на закладке интеграции появится новая интеграция "Мой газ" 42 | 43 | ![Установка mygas 4](images/setup-04.png) 44 | 45 | Также вы можете подключить несколько аккаунтов в интеграции. Они будут отображаться в настройках интеграции отдельными строками 46 | 47 | ![Установка mygas 5](images/setup-05.png) 48 | 49 | Щелкнув на один из лицевых счетов можно посмотреть устройства или объекты созданные для этого лицевого счета. 50 | 51 | ![Установка mygas 6](images/setup-06.png) 52 | 53 | Устройством будет каждый счетчик (прибора учета) в аккаунте. Объекты (сенсоры) для каждого лицевого счета 54 | 55 | Общий вид устройства в Home Assistant. 56 | 57 | ![Установка mygas 7](images/setup-07.png) 58 | 59 | # Сенсоры 60 | 61 | ![Сенсоры mygas 1](images/sensors-01.png) 62 | 63 | ![Сенсоры mygas 2](images/sensors-02.png) 64 | 65 | Создаются следующие объекты для каждого прибора учета: 66 | - `Лицевой счет` 67 | - `Задолженность` 68 | - `Последнее обновление` 69 | - `Счетчик` 70 | - `Средний расход` 71 | - `Цена за м³` 72 | - `Дата показаний` 73 | - `Показания` 74 | - `Потребление` 75 | 76 | Сенсор `Лицевой счет` имеет дополнительные атрибуты: 77 | - Площадь жилая 78 | - Количество проживающих 79 | - Использование среднегодовых тарифов 80 | - Наличие горячего водоснабжения 81 | - Электронная почта 82 | - Телефон 83 | - Адрес 84 | 85 | ![Сенсоры mygas 3](images/sensors-03.png) 86 | 87 | Сенсор `Счетчик` имеет дополнительные атрибуты: 88 | 89 | - Модель 90 | - Серийный номер 91 | - Состояние счетчика 92 | - Тип оборудования 93 | - Расположение 94 | - Ресурс 95 | - Тарифность 96 | - Дата очередной поверки 97 | - Плановая дата ТО 98 | - Дата установки пломбы 99 | - Дата заводской пломбы 100 | - Дата изготовления прибора 101 | 102 | ![Сенсоры mygas 4](images/sensors-04.png) 103 | 104 | ## Кнопки 105 | 106 | - **Обновить** - кнопка для немедленного обновления информации 107 | - Вызывает сервис `mygas.refresh`, сервис обновления информации 108 | - **Получить счет** - кнопка для запроса счета за прошлый месяц 109 | - Вызывает сервис `mygas.get_bill`, сервис запроса счета за прошлый месяц 110 | 111 | ![Установка mygas buttons](images/buttons-01.png) 112 | 113 | # Сервисы 114 | 115 | Интеграция Мой Газ публикует три сервиса: 116 | 117 | - `mygas.refresh` - сервис обновления информации 118 | - `mygas.get_bill` - сервис получения счета за прошлый месяц 119 | - `mygas.send_readings` - сервис отправки показаний 120 | 121 | ![Установка mygas services](images/services-01.png) 122 | 123 | ## mygas.refresh - Мой Газ: Обновить информацию 124 | 125 | Сервис запрашивает информацию через API и обновляет все сенсоры. 126 | 127 | ![Сервисы](images/services-02.png) 128 | 129 | Параметры: 130 | 131 | - **device_id** - Устройство Прибор учета 132 | 133 | Вызов сервиса в формате yaml 134 | 135 | ```yaml 136 | service: mygas.refresh 137 | data: 138 | device_id: 326995f8d0468225a1370b42297380c1 139 | ``` 140 | 141 | Можно сделать вызов сервиса с использованием имени устройства 142 | 143 | ```yaml 144 | service: mygas.get_bill 145 | data: 146 | device_id: '{{device_id("ВК-G6(Т) Krom Schloder, № 0XXXXXXX (000XXXXXX) (Офис)")}}' 147 | ``` 148 | 149 | или с использованием одного из сенсоров этого устройства 150 | 151 | ```yaml 152 | service: mygas.get_bill 153 | data: 154 | device_id: '{{device_id("sensor.mygas_XXXXXXXXXXXX_counter_XXXXXXXX_XXXX_XXXX_XXXX_XXXXXXXXXXXX_account")}}' 155 | ``` 156 | 157 | После завершения выполнения сервиса генерируется событие **mygas_refresh_completed**, 158 | в случае ошибки генерируется событие **mygas_refresh_failed**. 159 | 160 | 161 | ## mygas.get_bill - Мой Газ: Получить счет 162 | 163 | ![mygas services](images/services-03.png) 164 | 165 | Сервис позволяет запросить счет об оказанных услугах за прошлый месяц (по умолчанию) или на указанную дату. 166 | 167 | Параметры: 168 | 169 | - **device_id** - Устройство Прибор учета 170 | - **date** - Дата, на которую запрашивается счет, по умолчанию - первый день прошлого месяца. 171 | - **email** - электронная почта на которую будет отправлен счет, опционально 172 | 173 | Вызов сервиса в формате yaml 174 | 175 | ```yaml 176 | service: mygas.get_bill 177 | data: 178 | device_id: 326995f8d0468225a1370b42297380c1 179 | ``` 180 | или с указанием емейл 181 | 182 | ```yaml 183 | service: mygas.get_bill 184 | data: 185 | device_id: 326995f8d0468225a1370b42297380c1 186 | email: test@mail.ru 187 | ``` 188 | **Если была указана электронная почта, то запрошенный счет придет только на электронную почту** 189 | 190 | или с указанием даты 191 | 192 | ```yaml 193 | service: mygas.get_bill 194 | data: 195 | device_id: 326995f8d0468225a1370b42297380c1 196 | date: 2024-01-01 197 | ``` 198 | Вызов сервиса с указанием даты счета позволяет получить счет за указанный месяц. 199 | В этом случае необходимо указать первый день месяца, за который требуется получить счет. 200 | В случае если дата не указана, то будет получен счет за прошлый месяц. 201 | 202 | 203 | После завершения выполнения сервиса генерируется событие **mygas_get_bill_completed**, 204 | в случае ошибки генерируется событие **mygas_get_bill_failed**. 205 | 206 | ## mygas.send_readings - Мой Газ: Отправить показания 207 | 208 | ![Сервисы](images/services-04.png) 209 | 210 | Сервис отправляет показания в ТНС Энерго из указанных сенсоров. 211 | 212 | Параметры: 213 | 214 | - **device_id** - Устройство Лицевой счет 215 | - **value** - Сенсор со значением потребления 216 | 217 | Вызов сервиса в формате yaml 218 | 219 | ```yaml 220 | service: mygas.send_readings 221 | data: 222 | device_id: 326995f8d0468225a1370b42297380c1 223 | value: sensor.mygas_XXXXXXXXXXXX_counter_XXXXXXXX_XXXX_XXXX_XXXX_XXXXXXXXXXXX_readings 224 | 225 | ``` 226 | 227 | После завершения выполнения сервиса генерируется событие **mygas_send_readings_completed**, 228 | в случае ошибки генерируется событие **mygas_send_readings_failed**. 229 | 230 | # События 231 | 232 | Интеграция генерирует следующие события: 233 | 234 | - **mygas_refresh_completed** - сведения обновлены успешно 235 | - **mygas_get_bill_completed** - счет получен успешно 236 | - **mygas_send_readings_completed** - показания отправлены успешно 237 | - **mygas_refresh_failed** - возникла ошибка при обновлении сведений 238 | - **mygas_get_bill_failed** - возникла ошибка при получении счета 239 | - **mygas_send_readings_failed** - возникла ошибка при отправке показаний 240 | 241 | ## Событие: mygas_refresh_completed - Информация обновлена 242 | 243 | После выполнения службы обновления информации генерируется события **mygas_refresh_completed**, со следующими 244 | свойствами: 245 | 246 | ```yaml 247 | event_type: mygas_refresh_completed 248 | data: 249 | device_id: a05df6bea0854e17027c36a906722560 250 | origin: LOCAL 251 | time_fired: "2024-02-25T16:27:46.332645+00:00" 252 | context: 253 | id: 01HQGHG1ST4GVCR3FPJVCVTA6X 254 | parent_id: null 255 | user_id: 86cc507484e845f7b03f46eeaaab0fa7 256 | 257 | 258 | ``` 259 | ## Событие: mygas_get_bill_completed - Счет получен 260 | 261 | После успешного выполнения службы получения счета генерируется события **mygas_get_bill_completed**, со следующими 262 | свойствами: 263 | 264 | ```yaml 265 | event_type: mygas_get_bill_completed 266 | data: 267 | device_id: a05df6bea0854e17027c36a906722560 268 | date: "2024-01-01" 269 | url: >- 270 | https://xn--80asg7a0b.xn--80ahmohdapg.xn--80asehdb/prodcontainerone/xxxxxxxxxxxxx.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minio_service_user/xxxxxxxx/us-east-1/s3/aws4_request&X-Amz-Date=20240225T162907Z&X-Amz-Expires=600&X-Amz-SignedHeaders=host&X-Amz-Signature=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 271 | origin: LOCAL 272 | time_fired: "2024-02-25T16:29:07.414544+00:00" 273 | context: 274 | id: 01HQGHJHNGX3GNRNBQDBR7AXRX 275 | parent_id: null 276 | user_id: 86cc507484e845f7b03f46eeaaab0fa7 277 | 278 | 279 | ``` 280 | 281 | После успешного выполнения службы получения счета на электронную почту генерируется события **mygas_get_bill_completed**, со следующими 282 | свойствами: 283 | 284 | ```yaml 285 | event_type: mygas_get_bill_completed 286 | data: 287 | device_id: a05df6bea0854e17027c36a906722560 288 | date: "2024-01-01" 289 | email: your_email@mail.ru 290 | origin: LOCAL 291 | time_fired: "2024-02-25T16:29:07.414544+00:00" 292 | context: 293 | id: 01HQGHJHNGX3GNRNBQDBR7AXRX 294 | parent_id: null 295 | user_id: 86cc507484e845f7b03f46eeaaab0fa7 296 | 297 | 298 | ``` 299 | 300 | ## Событие: mygas_send_readings_completed - Показания отправлены 301 | 302 | После успешного выполнения службы отправки показаний генерируется события **mygas_send_readings_completed**, со следующими 303 | свойствами: 304 | 305 | ```yaml 306 | event_type: mygas_send_readings_completed 307 | data: 308 | device_id: a05df6bea0854e17027c36a906722560 309 | readings: 1806 310 | sent: true 311 | message: Показания счетчика успешно переданы 312 | origin: LOCAL 313 | time_fired: "2024-02-25T16:32:02.808445+00:00" 314 | context: 315 | id: 01HQGHQW60NNNTZD4GTSN21A8G 316 | parent_id: null 317 | user_id: 86cc507484e845f7b03f46eeaaab0fa7 318 | ``` 319 | 320 | ## Событие: mygas_*_failed - Запрос к сервису выполнился с ошибкой 321 | 322 | В случае выполнения сервиса с ошибкой интеграция генерирует следующие события: 323 | 324 | - **mygas_refresh_failed** - возникла ошибка при обновлении сведений 325 | - **mygas_get_bill_failed** - возникла ошибка при получении счета 326 | - **mygas_send_readings_failed** - возникла ошибка при отправке показаний 327 | 328 | Ниже пример такого события 329 | 330 | ```yaml 331 | event_type: mygas_refresh_failed 332 | data: 333 | device_id: 326995f8d0468225a1370b42297380c1 334 | error: "Error description" 335 | origin: LOCAL 336 | time_fired: "2024-02-21T17:18:09.428522+00:00" 337 | context: 338 | id: 01GYJD548GAG83ZEDVGVCF1WKR 339 | parent_id: null 340 | user_id: 386a6cba68ca41a0923d3b94b2710bdc 341 | ``` 342 | 343 | # Автоматизации 344 | 345 | ![Автоматизация](images/automations-01.png) 346 | 347 | Для отправки показаний и получения счета по расписанию можно создать автоматизации с использованием описанных выше служб, а 348 | также автоматизации для отправки уведомлений в Телеграм, и веб интерфейс Home Assistant. 349 | 350 | ## Вызов сервисов по расписанию 351 | 352 | Для вызова сервиса по расписанию используется платформа Time с дополнительным условием на дату. 353 | 354 | ### Отправка показаний в Мой Газ 355 | 356 | Показания будут отправляться 24 числа каждого месяца в 2 часа ночи, через час после отправки показаний 357 | будут обновляться сведения лицевого счета. 358 | 359 | ![Автоматизация](images/automations-02.png) 360 | 361 | ![Автоматизация](images/automations-03.png) 362 | 363 | Автоматизация в формате yaml 364 | 365 | ```yaml 366 | alias: "Мой Газ: Отправить показания по газу" 367 | description: Отправить показания по газу в сервис Мой Газ 368 | triggers: 369 | - at: "02:00:00" 370 | trigger: time 371 | conditions: 372 | - condition: template 373 | value_template: "{{ now().day == 24 }}" 374 | actions: 375 | - alias: "Мой Газ: Отправить показания" 376 | data: 377 | value: sensor.gaz_dom_pokazaniia 378 | device_id: 70e3a0546d0bdd5a8786bfb595def7db 379 | action: mygas.send_readings 380 | - delay: 381 | hours: 0 382 | minutes: 1 383 | seconds: 0 384 | milliseconds: 0 385 | - data: 386 | device_id: 70e3a0546d0bdd5a8786bfb595def7db 387 | action: mygas.refresh 388 | mode: single 389 | ``` 390 | Вы можете указать свою дату для этого скорректируйте строку `"{{ now().day == 24 }}"`, 391 | а также можно изменить время для этого в строке `at: "02:00:00"` укажите нужное время. 392 | 393 | 394 | ### Получение счета от Мой Газ 395 | 396 | Счет будет запрашиваться 5 числа каждого месяца в 2 часа ночи. 397 | 398 | ![Автоматизация](images/automations-05.png) 399 | 400 | Автоматизация в формате yaml 401 | 402 | ```yaml 403 | alias: Запросить счета за газ 404 | description: "" 405 | trigger: 406 | - platform: time 407 | at: "02:00:00" 408 | condition: 409 | - condition: template 410 | value_template: "{{ now().day == 5 }}" 411 | action: 412 | - service: mygas.get_bill 413 | data: 414 | device_id: 326995f8d0468225a1370b42297380c1 415 | mode: single 416 | ``` 417 | Вы можете указать свою дату для этого скорректируйте строку `"{{ now().day == 5 }}"`, 418 | а также можно изменить время для этого в строке `at: "01:00:00"` укажите нужное время. 419 | 420 | ## Уведомления 421 | 422 | Тригером для отправки уведомлений является соответсвующее событие **mygas_*_completed**. 423 | 424 | ### Уведомление об отправленных показаниях в Телеграм и веб интерфейс Home Assistant 425 | 426 | Автоматизация в формате yaml 427 | 428 | ```yaml 429 | alias: "Мой Газ: Уведомление об отправленных показаниях" 430 | description: "Уведомление об отправленных показаниях в сервис Мой Газ" 431 | triggers: 432 | - event_type: mygas_send_readings_completed 433 | trigger: event 434 | conditions: [] 435 | actions: 436 | - data: 437 | config_entry_id: # Replace with your Telegram Bot config entry ID 438 | message: "Показания: {{ trigger.event.data.readings }}" 439 | title: >- 440 | 🔥Газ. Показания для {{ 441 | device_attr(trigger.event.data.device_id,'name_by_user') or 442 | device_attr(trigger.event.data.device_id, 'name') }} отправлены {{ 443 | now().strftime('%d-%m-%Y %H:%M') }} 444 | action: telegram_bot.send_message 445 | - data: 446 | title: >- 447 | 🔥Газ. Показания для {{ 448 | device_attr(trigger.event.data.device_id,'name_by_user') or 449 | device_attr(trigger.event.data.device_id, 'name') }} отправлены {{ 450 | now().strftime("%d-%m-%Y %H:%M") }} 451 | message: "Показания: {{ trigger.event.data.readings }}" 452 | action: notify.persistent_notification 453 | mode: single 454 | 455 | 456 | ``` 457 | Результат выполнения - сообщение в Телеграм 458 | 459 | ![Автоматизация](images/telegram-01.png) 460 | 461 | Результат выполнения - уведомление в Home Assistant 462 | 463 | ![Автоматизация](images/notify-01.png) 464 | 465 | Получить `` можно на странице настроек вашего бота в Home Assistant. 466 | 467 | ![Автоматизация](images/telegram-01_01.png) 468 | 469 | ### Уведомления о счете за газ 470 | 471 | Автоматизация в формате yaml 472 | 473 | ```yaml 474 | lias: "Мой Газ: Уведомление о счете за газ" 475 | description: Уведомление о счете за газ от сервиса Мой Газ 476 | triggers: 477 | - event_type: mygas_get_bill_completed 478 | trigger: event 479 | conditions: [] 480 | actions: 481 | - data: 482 | url: "{{trigger.event.data.url}}" 483 | caption: >- 484 | 🔥Газ. Счет для {{device_attr(trigger.event.data.device_id, 485 | 'name_by_user') or device_attr(trigger.event.data.device_id, 'name') }} 486 | за {{trigger.event.data.date}} 487 | config_entry_id: # Replace with your Telegram Bot config entry ID 488 | action: telegram_bot.send_document 489 | - data: 490 | message: >- 491 | Скачать счет для [{{device_attr(trigger.event.data.device_id, 492 | 'name_by_user') or device_attr(trigger.event.data.device_id, 'name') 493 | }}]({{trigger.event.data.url}}) за {{trigger.event.data.date}}. 494 | title: >- 495 | 🔥Газ. Счет для {{device_attr(trigger.event.data.device_id, 496 | 'name_by_user') or device_attr(trigger.event.data.device_id, 'name') }} 497 | за {{trigger.event.data.date}} 498 | action: notify.persistent_notification 499 | mode: single 500 | 501 | 502 | ``` 503 | 504 | Результат выполнения - сообщение в Телеграм 505 | 506 | ![Автоматизация](images/telegram-02.png) 507 | 508 | При отправке оповещения в телеграм передается не временная ссылка на сгенерированный счет, а сам счет. 509 | 510 | Результат выполнения - уведомление в Home Assistant 511 | 512 | ![Автоматизация](images/notify-02.png) 513 | 514 | Автоматизация в формате yaml для получения уведомлений об отправке на электронную почту 515 | ```yaml 516 | alias: "Мой Газ: Уведомление о счете за газ на электронную почту" 517 | description: Уведомление о счете за газ на электронную почту от сервиса Мой Газ 518 | triggers: 519 | - event_type: mygas_get_bill_completed 520 | trigger: event 521 | conditions: 522 | - condition: template 523 | value_template: "{{ trigger.event.data.email != none }}" 524 | actions: 525 | - data: 526 | message: >- 527 | 🔥Газ. Счет для {{device_attr(trigger.event.data.device_id, 528 | 'name_by_user') or device_attr(trigger.event.data.device_id, 'name') }} 529 | за {{trigger.event.data.date}} 530 | config_entry_id: # Replace with your Telegram Bot config entry ID 531 | action: telegram_bot.send_message 532 | - data: 533 | message: >- 534 | 🔥Газ. Счет для {{device_attr(trigger.event.data.device_id, 535 | 'name_by_user') or device_attr(trigger.event.data.device_id, 'name') }} 536 | за {{trigger.event.data.date}} отправлен на 537 | {{trigger.event.data.email}}. 538 | action: notify.persistent_notification 539 | mode: single 540 | 541 | 542 | ``` 543 | 544 | ### Уведомления об ошибках, возникших в процессе выполнения сервиса 545 | 546 | Уведомления об ошибках, возникших в процессе выполнения, в Телеграм и веб интерфейс Home Assistant. 547 | 548 | ```yaml 549 | alias: "Мой Газ: Уведомление об ошибке при выполнения сервиса" 550 | description: Уведомление об ошибке при выполнения сервиса Мой Газ 551 | triggers: 552 | - event_type: mygas_send_readings_failed 553 | trigger: event 554 | - event_type: mygas_get_bill_failed 555 | trigger: event 556 | - event_type: mygas_refresh_failed 557 | trigger: event 558 | conditions: [] 559 | actions: 560 | - data: 561 | config_entry_id: # Replace with your Telegram Bot config entry ID 562 | message: "{{ trigger.event.data.error }}" 563 | title: >- 564 | 🔥Газ. {% if trigger.event.event_type == 'mygas_send_readings_failed' %} 565 | Ошибка при передаче показаний для {% elif trigger.event.event_type 566 | == 'mygas_get_bill_failed' %} Ошибка при получении счета для {% elif 567 | trigger.event.event_type == 'mygas_refresh_failed' %} Ошибка при 568 | обновлении информации для {% else %} Ошибка при выполнении сервиса для 569 | {% endif %} {{device_attr(trigger.event.data.device_id, 570 | 'name_by_user') or device_attr(trigger.event.data.device_id, 'name') 571 | }} от {{ now().strftime('%d-%m-%Y %H:%M')}} 572 | action: telegram_bot.send_message 573 | - data: 574 | title: >- 575 | 🔥Газ. {% if trigger.event.event_type == 'mygas_send_readings_failed' %} 576 | Ошибка при передаче показаний для {% elif trigger.event.event_type == 577 | 'mygas_get_bill_failed' %} Ошибка при получении счета для {% elif 578 | trigger.event.event_type == 'mygas_refresh_failed' %} Ошибка при 579 | обновлении информации для {% else %} Ошибка при выполнении сервиса для 580 | {% endif %} {{device_attr(trigger.event.data.device_id, 'name_by_user') 581 | or device_attr(trigger.event.data.device_id, 'name') }} от {{ 582 | now().strftime('%d-%m-%Y %H:%M')}} 583 | message: "{{ trigger.event.data.error }}" 584 | action: notify.persistent_notification 585 | mode: single 586 | 587 | ``` 588 | 589 | ## Получение счёта за газ с выбором даты через интерфейс 590 | 591 | Интеграция `mygas` поддерживает сервис `mygas.get_bill`, который принимает параметры `device_id` и `date`. 592 | Для удобства можно создать пользовательский интерфейс с выбором даты и кнопкой вызова сервиса. 593 | 594 | ### Настройка помощника (Helper) 595 | 596 | Создайте помощник `input_datetime`, чтобы выбрать дату: 597 | 598 | 1. Откройте **Settings → Devices & Services → Helpers** 599 | 2. Нажмите **+ Create Helper** 600 | 3. Выберите **Date** 601 | 4. Укажите имя, например: `gas_bill_date` 602 | 603 | После этого будет создан объект `input_datetime.gas_bill_date`. 604 | 605 | 606 | ### Создание скрипта 607 | 608 | Создайте скрипт `get_gas_bill` (через UI или `scripts.yaml`), который вызывает сервис `mygas.get_bill` с параметрами: 609 | 610 | ```yaml 611 | sequence: 612 | - service: mygas.get_bill 613 | data: 614 | device_id: 326995f8d0468225a1370b42297380c1 615 | date: "{{ states('input_datetime.gas_bill_date') }}" 616 | ``` 617 | 618 | > Замените `device_id` на ваш фактический ID устройства. 619 | 620 | 621 | ### Добавление в интерфейс Lovelace 622 | 623 | Добавьте следующую карточку в интерфейс Home Assistant (Lovelace UI): 624 | 625 | ```yaml 626 | type: vertical-stack 627 | cards: 628 | - type: entities 629 | entities: 630 | - entity: input_datetime.gas_bill_date 631 | - type: button 632 | name: Получить счёт 633 | icon: mdi:file-document 634 | tap_action: 635 | action: call-service 636 | service: script.get_gas_bill 637 | show_name: false 638 | show_icon: true 639 | show_state: false 640 | ``` 641 | 642 | ![get_gas-bill-date-lovelace.png](images/get_gas-bill-date-lovelace.png) 643 | 644 | ### Использование 645 | 646 | 1. Выберите дату в календаре 647 | 2. Нажмите кнопку **Получить счёт** 648 | 3. Сервис `mygas.get_bill` будет вызван с выбранной датой 649 | 650 | 651 | ### Примечание 652 | 653 | Подстановки вида `{{ states(...) }}` **не поддерживаются напрямую** в кнопках интерфейса Lovelace. Чтобы передавать параметры с шаблонами, используйте **скрипты**. 654 | 655 | ## Получение счёта за газ на сегодня 656 | 657 | Если вы хотите, чтобы сервис `mygas.get_bill` автоматически запрашивал счёт за сегодня, без выбора даты вручную, 658 | вы можете создать скрипт или автоматизацию, которая будет передавать текущую дату в формате `YYYY-MM-DD`. 659 | 660 | ### Скрипт 661 | 662 | Создайте следующий скрипт `get_gas_bill_today`: 663 | 664 | ```yaml 665 | sequence: 666 | - service: mygas.get_bill 667 | data: 668 | device_id: 67f641f60d6e5670a54271e4c71aa051 669 | date: "{{ now().date() }}" 670 | ``` 671 | 672 | `{{ now().date() }}` возвращает текущую дату в формате YYYY-MM-DD. 673 | 674 | ### Кнопка в интерфейсе Lovelace 675 | 676 | Добавьте кнопку в интерфейс для запуска: 677 | 678 | ```yaml 679 | type: button 680 | name: Получить счёт за сегодня 681 | icon: mdi:calendar-today 682 | tap_action: 683 | action: call-service 684 | service: script.get_gas_bill_today 685 | ``` 686 | 687 | ## Возникли проблемы? 688 | 689 | Включите ведение журнала отладки, поместив следующие инструкции в файл configuration.yaml: 690 | 691 | ```yaml 692 | logger: 693 | default: warning 694 | logs: 695 | custom_components.mygas: debug 696 | aiomygas: debug 697 | 698 | ``` 699 | После возникновения проблемы, пожалуйста, найдите проблему в журнале (/config/home-assistant.log) и 700 | создайте [запрос на исправление](https://github.com/lizardsystems/hass-mygas/issues). 701 | 702 | # Дополнительная информация 703 | 704 | Эта интеграция использует API [Мой Газ Смородина](https://мойгаз.смородина.онлайн/). 705 | --------------------------------------------------------------------------------