Compare commits

..
67 Commits
Author SHA1 Message Date
Patrick762 0943ecf187 fix manifest version number 2024-02-10 14:20:37 +01:00
Patrick762 58f2ec50c4 added sleep() between pack pollings 2024-02-10 14:15:52 +01:00
Patrick762 ad536235bb removed device name from entity name 2024-02-10 13:59:01 +01:00
Patrick762 ba6611b084 removed duplicate re.sub() 2024-02-10 13:21:49 +01:00
Patrick762 3e4b8601e8 added dropdown to select device model 2024-02-10 13:19:16 +01:00
Patrick762 ebbcea3c4c fixed circular import 2024-02-05 19:10:11 +01:00
Patrick762 9ef9d38a6e renamed ep700 to ep760 2024-02-04 12:05:53 +01:00
Patrick762 463f02b09b Merge pull request #37 from Patrick762/ep700
Added EP700
2024-02-03 18:01:52 +01:00
Patrick762 8eb09a32f0 added ep700, replaced dummy device 2024-02-03 17:57:49 +01:00
Patrick762 e8b9db9a42 Delete old issue template 2024-01-09 20:57:42 +01:00
Patrick762 9c7e5968bb Update bug_report.yaml 2024-01-09 20:56:27 +01:00
Patrick762 0420511d90 Update and rename bug_report.md to bug_report_template.md 2024-01-09 20:53:24 +01:00
Patrick762 dcc6131d02 Create bug_report.yaml 2024-01-09 20:49:56 +01:00
Patrick762 11716084c9 Update issue templates 2024-01-09 20:39:17 +01:00
Patrick762 aa1c3ef425 Update issue templates 2024-01-09 20:35:49 +01:00
Patrick762 a8931e09c7 added disclaimer 2024-01-06 15:46:04 +01:00
Patrick762 66a7de9051 added has_entity_name for compatibility reasons 2024-01-06 15:39:08 +01:00
Patrick762 528378ed9f Merge pull request #19 from Patrick762/dev
Version 0.0.11
2024-01-05 20:58:12 +01:00
Patrick762 b7c162aaef updated readme 2024-01-04 16:02:49 +01:00
Patrick762 dda0d475f4 stop notifier before disconnect 2024-01-04 15:39:02 +01:00
Patrick762 98f004dad1 revert to previous version number 2024-01-04 15:16:15 +01:00
Patrick762 515a06dc3a added more config options 2024-01-04 15:14:36 +01:00
Patrick762 14f2219b94 updated version number, added logging 2023-12-17 21:08:28 +01:00
Patrick762 4710a8183f replaced deploy script 2023-12-17 16:55:41 +01:00
Patrick762 186d73ace1 Merge pull request #24 from ViPeR5000/patch-1
Create pt.json
2023-12-17 16:53:58 +01:00
Rui Melo 7c211cde6f Create pt.json 2023-12-13 21:11:54 +00:00
Patrick762 4f99b7d746 Added install instructions for integration 2023-12-11 19:55:44 +01:00
Patrick762 70d7403aec Added stale bot action 2023-12-10 16:50:47 +01:00
Patrick762 33fe91c2d2 added check if packs available 2023-09-23 19:45:02 +02:00
Patrick762 7904a07e7f added ep600 dc input, prevent pack polling 2023-09-23 17:52:18 +02:00
Patrick762 18e48092c0 Merge pull request #15 from SupaFlav/dev
Add battery pack percent & voltage
2023-09-23 15:47:55 +02:00
Patrick762 792da76a04 Merge pull request #18 from Patrick762/Patrick762-patch-1
Update README.md
2023-09-23 15:26:25 +02:00
Patrick762 107ac9f0c8 Update README.md 2023-09-23 15:25:42 +02:00
SupaFlav 3d8cf4b98f Add 5x reconnect attempts on poll 2023-09-08 19:56:12 -04:00
SupaFlav c3afdcfe89 Allow switches with persistent_conn and add mutex to guard polling 2023-09-03 23:33:26 -04:00
SupaFlav d38c30011c return empty bytes object vs None on exception 2023-09-01 09:51:57 -04:00
SupaFlav 32cfc34c37 refactor send command & return response within new func 2023-08-31 23:42:00 -04:00
SupaFlav 7c25cbbe27 remove commentary 2023-08-30 18:13:51 -04:00
SupaFlav 57d0969701 Merge pull request #1 from Patrick762/dev
Pull latest from upstream
2023-08-30 15:42:27 -04:00
SupaFlav b8a73e0ab9 add polling for battery pack voltage & percent 2023-08-30 15:34:22 -04:00
Patrick762 39c75b0f75 update version number 2023-08-30 21:02:39 +02:00
Patrick762 3dc1cff551 Merge pull request #14 from SupaFlav/dev
Remove redundant connection attempt & don't process callback data on dead connection with persistent_conn active
2023-08-30 20:58:58 +02:00
SupaFlav 296c9cbfd3 Ensure this only applies to persistent connections 2023-08-28 15:33:45 -04:00
SupaFlav 0cadbb9931 Remove extra connection attempt, rm callback debug login when not connected 2023-08-28 15:03:43 -04:00
Patrick762 3a662964b8 Merge pull request #13 from Patrick762/dev
Config options
2023-08-26 20:31:22 +02:00
Patrick762 43be1e1dbe added utility to make macs loggable 2023-08-26 18:01:05 +02:00
Patrick762 e0c7ada147 updated version number 2023-08-26 16:00:41 +02:00
Patrick762 cf43c55cb8 fixed error on first device setup 2023-08-26 15:56:10 +02:00
Patrick762 a5ced35d32 added validation for polling interval 2023-08-26 15:54:33 +02:00
Patrick762 15216e1268 added option for persistant connection 2023-08-26 15:25:14 +02:00
Patrick762 ed10127a90 added option to set polling interval 2023-08-26 14:44:03 +02:00
Patrick762 4c286324c7 Merge pull request #12 from Patrick762/dev
increased version number
2023-08-19 22:42:07 +02:00
Patrick762 6361e661c0 increased version number 2023-08-19 22:41:39 +02:00
Patrick762 0d237f2e45 Merge pull request #11 from Patrick762/dev
Improvements and bug fixes
2023-08-19 22:40:40 +02:00
Patrick762 95f2331cc3 fixed enum sensors 2023-08-19 22:38:30 +02:00
Patrick762 61bc379d3a updated readme 2023-08-19 21:20:32 +02:00
Patrick762 dd7665f069 added option to turn on and off outputs 2023-08-19 21:17:44 +02:00
Patrick762 8fce8270b2 added switches for ac and dc on off 2023-08-19 17:38:24 +02:00
Patrick762 886ab957bb added options flow 2023-08-19 17:06:58 +02:00
Patrick762 5a6dbb8d00 added "my" link to readme 2023-08-19 17:05:59 +02:00
Patrick762 d54bf7c526 Merge pull request #7 from Patrick762/dev
Added more EP600 fields
2023-08-04 19:49:33 +02:00
Patrick762 22c2f0c7d0 edited comment 2023-08-04 19:49:22 +02:00
Patrick762 23dc2b7f41 renamed some fields, added grid input power fields 2023-08-04 19:47:42 +02:00
Patrick762 84a742968a added more ep600 fields 2023-08-04 19:27:58 +02:00
Patrick762 ed818070c2 Merge pull request #6 from Patrick762/dev
Updated EP600 fields
2023-08-03 22:01:00 +02:00
Patrick762 beb4e6b8fb added input and output frequency for ep600 2023-08-03 21:58:59 +02:00
Patrick762 7444097c60 added more ep600 voltage entities 2023-08-03 21:44:51 +02:00
19 changed files with 1400 additions and 170 deletions

No files matched your search

+76
View File
@@ -0,0 +1,76 @@
name: Bug Report
description: File a bug report
title: "[Bug]: "
labels: ["bug"]
assignees:
- octocat
body:
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Also tell us, what did you expect to happen?
placeholder: Tell us what you see!
value: "An exception appeared"
validations:
required: true
- type: dropdown
id: version
attributes:
label: Version
description: What version of our software are you running?
options:
- 0.0.11
- 0.0.10
- 0.0.9
default: 0
validations:
required: true
- type: dropdown
id: device
attributes:
label: What device are you seeing the problem on?
multiple: true
options:
- AC60
- AC200M
- AC300
- AC500
- EB3A
- EP500
- EP500P
- EP900
validations:
required: true
- type: dropdown
id: bluetooth
attributes:
label: What bluetooth connection are you using?
multiple: false
options:
- USB dongle
- ESPHome bluetooth proxy
validations:
required: true
- type: dropdown
id: options-changed
attributes:
label: Have you changed the integration settings?
multiple: false
options:
- No
- Yes (Please write down the changed settings in the next step)
validations:
required: true
- type: textarea
id: options-details
attributes:
label: Integration settings (if you changed them)
description: Please use one line per option
render: text
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell
+22
View File
@@ -0,0 +1,22 @@
name: Close inactive issues
on:
schedule:
- cron: "30 1 * * *"
jobs:
close-issues:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v5
with:
days-before-issue-stale: 30
days-before-issue-close: 14
stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
days-before-pr-stale: -1
days-before-pr-close: -1
repo-token: ${{ secrets.GITHUB_TOKEN }}
+16 -4
View File
@@ -1,13 +1,21 @@
# hassio-bluetti-bt # hassio-bluetti-bt
[![hacs_badge](https://img.shields.io/badge/HACS-Custom-41BDF5.svg)](https://github.com/hacs/integration) [![hacs_badge](https://img.shields.io/badge/HACS-Default-41BDF5.svg)](https://github.com/hacs/integration)
[![Validate with hassfest](https://github.com/Patrick762/hassio-bluetti-bt/actions/workflows/hassfest_validation.yml/badge.svg)](https://github.com/Patrick762/hassio-bluetti-bt/actions/workflows/hassfest_validation.yml) [![Validate with hassfest](https://github.com/Patrick762/hassio-bluetti-bt/actions/workflows/hassfest_validation.yml/badge.svg)](https://github.com/Patrick762/hassio-bluetti-bt/actions/workflows/hassfest_validation.yml)
[![HACS Action](https://github.com/Patrick762/hassio-bluetti-bt/actions/workflows/HACS.yml/badge.svg)](https://github.com/Patrick762/hassio-bluetti-bt/actions/workflows/HACS.yml) [![HACS Action](https://github.com/Patrick762/hassio-bluetti-bt/actions/workflows/HACS.yml/badge.svg)](https://github.com/Patrick762/hassio-bluetti-bt/actions/workflows/HACS.yml)
Bluetti Integration for Home Assistant Bluetti Integration for Home Assistant
Based on [bluetti_mqtt](https://github.com/warhammerkid/bluetti_mqtt). Based on [bluetti_mqtt](https://github.com/warhammerkid/bluetti_mqtt).
## Disclaimer
This integration is provided without any warranty or support by Bluetti (unfortunately). I do not take responsibility for any problems it may cause in all cases. Use it at your own risk.
## Installation
To install this integration, you first need [HACS](https://hacs.xyz/) installed.
After the installation, you can use this button to install the integration:
[![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=Patrick762&repository=hassio-bluetti-bt&category=integration)
### Supported devices: ### Supported devices:
(based on [supported devices of bluetti_mqtt](https://github.com/warhammerkid/bluetti_mqtt/tree/main/bluetti_mqtt/core/devices)) (based on [supported devices of bluetti_mqtt](https://github.com/warhammerkid/bluetti_mqtt/tree/main/bluetti_mqtt/core/devices))
@@ -19,7 +27,11 @@ Based on [bluetti_mqtt](https://github.com/warhammerkid/bluetti_mqtt).
- EP500 - EP500
- EP500P - EP500P
- EP600 (tested) - EP600 (tested)
- EP760 (beta, untested)
### Available sensors: ### Available sensors:
All sensors which are available in bluetti_mqtt (Based on [this file](https://github.com/warhammerkid/bluetti_mqtt/blob/main/bluetti_mqtt/mqtt_client.py)) All sensors which are available in bluetti_mqtt (Based on [this file](https://github.com/warhammerkid/bluetti_mqtt/blob/main/bluetti_mqtt/mqtt_client.py)).
excluding battery pack fields.
### Available controls:
If enabled in the Integration options (you need to reload the integration if you change this option):
AC and DC outputs
+33 -7
View File
@@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
import re import re
import logging
from homeassistant.components import bluetooth from homeassistant.components import bluetooth
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@@ -11,10 +12,21 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from .const import DOMAIN, MANUFACTURER from .const import (
CONF_MAX_RETRIES,
CONF_PERSISTENT_CONN,
CONF_POLLING_INTERVAL,
CONF_POLLING_TIMEOUT,
CONF_USE_CONTROLS,
DATA_COORDINATOR,
DATA_POLLING_RUNNING,
DOMAIN,
MANUFACTURER,
)
from .coordinator import PollingCoordinator from .coordinator import PollingCoordinator
PLATFORMS: [Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] PLATFORMS: [Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR]
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@@ -22,6 +34,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
address = entry.data.get(CONF_ADDRESS) address = entry.data.get(CONF_ADDRESS)
device_name = entry.data.get(CONF_NAME) device_name = entry.data.get(CONF_NAME)
use_controls = entry.data.get(CONF_USE_CONTROLS)
polling_interval = entry.data.get(CONF_POLLING_INTERVAL, 20)
persistent_conn = entry.data.get(CONF_PERSISTENT_CONN, False)
polling_timeout = entry.data.get(CONF_POLLING_TIMEOUT, 45)
max_retries = entry.data.get(CONF_MAX_RETRIES, 5)
if address is None: if address is None:
return False return False
@@ -29,16 +46,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if not bluetooth.async_address_present(hass, address): if not bluetooth.async_address_present(hass, address):
raise ConfigEntryNotReady("Bluetti device not present") raise ConfigEntryNotReady("Bluetti device not present")
# Create coordinator for polling
coordinator = PollingCoordinator(hass, address, device_name)
await coordinator.async_config_entry_first_refresh()
# Create data structure # Create data structure
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN].setdefault(entry.entry_id, coordinator) hass.data[DOMAIN].setdefault(entry.entry_id, {})
hass.data[DOMAIN][entry.entry_id].setdefault(DATA_POLLING_RUNNING, False)
# Create coordinator for polling
coordinator = PollingCoordinator(hass, address, device_name, polling_interval, persistent_conn, polling_timeout, max_retries)
await coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN][entry.entry_id].setdefault(DATA_COORDINATOR, coordinator)
platforms: list = PLATFORMS
if use_controls is True:
_LOGGER.warning("You are using controls with this integration at your own risk!")
platforms.append(Platform.SWITCH)
# Setup platforms # Setup platforms
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, platforms)
return True return True
@@ -78,4 +102,6 @@ def get_type_by_bt_name(bt_name: str):
dev_type = "EP500" dev_type = "EP500"
elif bt_name.startswith("EP600"): elif bt_name.startswith("EP600"):
dev_type = "EP600" dev_type = "EP600"
elif bt_name.startswith("EP760"):
dev_type = "EP760"
return dev_type return dev_type
+17 -13
View File
@@ -18,7 +18,6 @@ from homeassistant.helpers.update_coordinator import (
CoordinatorEntity, CoordinatorEntity,
) )
from bluetti_mqtt.bluetooth import build_device
from bluetti_mqtt.mqtt_client import ( from bluetti_mqtt.mqtt_client import (
NORMAL_DEVICE_FIELDS, NORMAL_DEVICE_FIELDS,
DC_INPUT_FIELDS, DC_INPUT_FIELDS,
@@ -26,8 +25,9 @@ from bluetti_mqtt.mqtt_client import (
) )
from . import device_info as dev_info, get_unique_id from . import device_info as dev_info, get_unique_id
from .const import DOMAIN, ADDITIONAL_DEVICE_FIELDS from .const import DATA_COORDINATOR, DOMAIN, ADDITIONAL_DEVICE_FIELDS
from .coordinator import PollingCoordinator, DummyDevice from .coordinator import PollingCoordinator
from .utils import unique_id_loggable, build_device
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -35,7 +35,7 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Setup sensor entities.""" """Setup binary_sensor entities."""
device_name = entry.data.get(CONF_NAME) device_name = entry.data.get(CONF_NAME)
address = entry.data.get(CONF_ADDRESS) address = entry.data.get(CONF_ADDRESS)
@@ -43,12 +43,11 @@ async def async_setup_entry(
_LOGGER.error("Device has no address") _LOGGER.error("Device has no address")
# Generate device info # Generate device info
_LOGGER.info("Creating sensors for device with address %s", address) _LOGGER.info("Creating binary_sensors for device with address %s", address)
device_info = dev_info(entry) device_info = dev_info(entry)
# Add sensors according to device_info # Add sensors according to device_info
bluetti_device = build_device(address, device_name) bluetti_device = build_device(address, device_name)
bluetti_device = DummyDevice(bluetti_device)
sensors_to_add = [] sensors_to_add = []
all_fields = NORMAL_DEVICE_FIELDS all_fields = NORMAL_DEVICE_FIELDS
@@ -63,7 +62,7 @@ async def async_setup_entry(
sensors_to_add.append( sensors_to_add.append(
BluettiBinarySensor( BluettiBinarySensor(
hass.data[DOMAIN][entry.entry_id], hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR],
device_info, device_info,
address, address,
field_key, field_key,
@@ -90,12 +89,13 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity):
"""Init battery entity.""" """Init battery entity."""
super().__init__(coordinator) super().__init__(coordinator)
self._attr_has_entity_name = True
e_name = f"{device_info.get('name')} {name}" e_name = f"{device_info.get('name')} {name}"
self._address = address self._address = address
self._response_key = response_key self._response_key = response_key
self._attr_device_info = device_info self._attr_device_info = device_info
self._attr_name = e_name self._attr_name = name
self._attr_available = False self._attr_available = False
self._attr_unique_id = get_unique_id(e_name) self._attr_unique_id = get_unique_id(e_name)
self._attr_entity_category = category self._attr_entity_category = category
@@ -103,10 +103,14 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity):
@callback @callback
def _handle_coordinator_update(self) -> None: def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator.""" """Handle updated data from the coordinator."""
_LOGGER.debug("Updating state of %s", self._attr_unique_id)
if self.coordinator.persistent_conn and not self.coordinator.client.is_connected:
return
_LOGGER.debug("Updating state of %s", unique_id_loggable(self._attr_unique_id))
if not isinstance(self.coordinator.data, dict): if not isinstance(self.coordinator.data, dict):
_LOGGER.error( _LOGGER.debug(
"Invalid data from coordinator (sensor.%s)", self._attr_unique_id "Invalid data from coordinator (binary_sensor.%s)", unique_id_loggable(self._attr_unique_id)
) )
self._attr_available = False self._attr_available = False
return return
@@ -118,8 +122,8 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity):
if not isinstance(response_data, bool): if not isinstance(response_data, bool):
_LOGGER.warning( _LOGGER.warning(
"Invalid response data type from coordinator (sensor.%s): %s", "Invalid response data type from coordinator (binary_sensor.%s): %s",
self._attr_unique_id, unique_id_loggable(self._attr_unique_id),
response_data, response_data,
) )
self._attr_available = False self._attr_available = False
+114 -3
View File
@@ -14,9 +14,18 @@ from homeassistant.components.bluetooth import (
) )
from homeassistant.const import CONF_ADDRESS, CONF_TYPE, CONF_NAME from homeassistant.const import CONF_ADDRESS, CONF_TYPE, CONF_NAME
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import selector
from . import get_type_by_bt_name from . import get_type_by_bt_name
from .const import DOMAIN from .const import (
CONF_MAX_RETRIES,
CONF_PERSISTENT_CONN,
CONF_POLLING_INTERVAL,
CONF_POLLING_TIMEOUT,
CONF_USE_CONTROLS,
DOMAIN,
SUPPORTED_MODELS,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -50,12 +59,18 @@ class BluettiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(address, raise_on_progress=False) await self.async_set_unique_id(address, raise_on_progress=False)
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
dev_type = get_type_by_bt_name(discovery_info.name) dev_type = get_type_by_bt_name(discovery_info.name)
name = re.sub("[^A-Z0-9]+", "", discovery_info.name)
if dev_type == "Unknown":
dev_type = user_input[CONF_TYPE]
name = name.replace("PBOX", dev_type)
return self.async_create_entry( return self.async_create_entry(
title=discovery_info.name, title=name,
data={ data={
CONF_ADDRESS: discovery_info.address, CONF_ADDRESS: discovery_info.address,
CONF_TYPE: dev_type, CONF_TYPE: dev_type,
CONF_NAME: re.sub("[^A-Z0-9]+", "", discovery_info.name), CONF_NAME: name,
}, },
) )
@@ -80,9 +95,105 @@ class BluettiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
for service_info in self._discovered_devices.values() for service_info in self._discovered_devices.values()
} }
), ),
vol.Optional(CONF_TYPE): vol.In(
{
model: model
for model in SUPPORTED_MODELS
}
),
} }
) )
return self.async_show_form( return self.async_show_form(
step_id="user", step_id="user",
data_schema=data_schema, data_schema=data_schema,
) )
@staticmethod
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> config_entries.OptionsFlow:
"""Create the options flow."""
return OptionsFlowHandler(config_entry)
class OptionsFlowHandler(config_entries.OptionsFlow):
"""Handle a option flow."""
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow."""
self.config_entry = config_entry
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the options."""
if user_input is not None:
# Validate update interval
if user_input[CONF_POLLING_INTERVAL] < 5:
return self.async_abort(reason="invalid_interval")
# Validate update timeout
if user_input[CONF_POLLING_TIMEOUT] < 1:
return self.async_abort(reason="invalid_timeout")
# Validate max retries
if user_input[CONF_MAX_RETRIES] < 1:
return self.async_abort(reason="invalid_retries")
changed = self.hass.config_entries.async_update_entry(
self.config_entry,
data={
**self.config_entry.data,
**{
CONF_USE_CONTROLS: user_input[CONF_USE_CONTROLS],
CONF_PERSISTENT_CONN: user_input[CONF_PERSISTENT_CONN],
CONF_POLLING_INTERVAL: user_input[CONF_POLLING_INTERVAL],
CONF_POLLING_TIMEOUT: user_input[CONF_POLLING_TIMEOUT],
CONF_MAX_RETRIES: user_input[CONF_MAX_RETRIES],
},
},
)
if changed is False:
_LOGGER.error(
"Method OptionsFlowHandler.async_step_init: Config entry %s has not been changed",
self.config_entry.entry_id,
)
return self.async_create_entry(
title="",
data={
CONF_USE_CONTROLS: user_input[CONF_USE_CONTROLS],
CONF_PERSISTENT_CONN: user_input[CONF_PERSISTENT_CONN],
CONF_POLLING_INTERVAL: user_input[CONF_POLLING_INTERVAL],
CONF_POLLING_TIMEOUT: user_input[CONF_POLLING_TIMEOUT],
CONF_MAX_RETRIES: user_input[CONF_MAX_RETRIES],
},
)
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Required(
CONF_USE_CONTROLS,
default=self.config_entry.data.get(CONF_USE_CONTROLS, False),
): selector.BooleanSelector(),
vol.Required(
CONF_PERSISTENT_CONN,
default=self.config_entry.data.get(CONF_PERSISTENT_CONN, False),
): selector.BooleanSelector(),
vol.Required(
CONF_POLLING_INTERVAL,
default=self.config_entry.data.get(CONF_POLLING_INTERVAL, 20),
): int,
vol.Required(
CONF_POLLING_TIMEOUT,
default=self.config_entry.data.get(CONF_POLLING_TIMEOUT, 45),
): int,
vol.Required(
CONF_MAX_RETRIES,
default=self.config_entry.data.get(CONF_MAX_RETRIES, 5),
): int,
}
),
)
+444
View File
@@ -1,11 +1,39 @@
"""Constants for the Bluetti BT integration.""" """Constants for the Bluetti BT integration."""
import re
from bluetti_mqtt.mqtt_client import MqttFieldConfig, MqttFieldType from bluetti_mqtt.mqtt_client import MqttFieldConfig, MqttFieldType
DOMAIN = "bluetti_bt" DOMAIN = "bluetti_bt"
MANUFACTURER = "Bluetti" MANUFACTURER = "Bluetti"
CONF_OPTIONS = "options" CONF_OPTIONS = "options"
CONF_USE_CONTROLS = "use_controls"
CONF_PERSISTENT_CONN = "persistent_conn"
CONF_POLLING_INTERVAL = "polling_interval"
CONF_POLLING_TIMEOUT = "polling_timeout"
CONF_MAX_RETRIES = "max_retries"
DATA_COORDINATOR = "coordinator"
DATA_POLLING_RUNNING = "polling_running"
SUPPORTED_MODELS = [
"AC60",
"AC200M",
"AC300",
"AC500",
"EB3A",
"EP500",
"EP500P",
"EP600",
"EP760",
]
DEVICE_NAME_RE = re.compile(r'^(AC200M|AC300|AC500|AC60|EP500P|EP500|EP600|EP760|EB3A)(\d+)$')
CONTROL_FIELDS = [
"ac_output_on",
"dc_output_on",
]
DIAGNOSTIC_FIELDS = [ DIAGNOSTIC_FIELDS = [
"max_ac_input_power", "max_ac_input_power",
@@ -68,6 +96,114 @@ ADDITIONAL_DEVICE_FIELDS = {
advanced=False, advanced=False,
home_assistant_extra={"name": "High Voltage Safety Module Version"}, home_assistant_extra={"name": "High Voltage Safety Module Version"},
), ),
'pv_input_power1': MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Solar Input Power 1",
"unit_of_measurement": "W",
"device_class": "power",
"state_class": "measurement",
"force_update": True,
},
),
'pv_input_voltage1': MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Solar Input Voltage 1",
"unit_of_measurement": "V",
"device_class": "voltage",
"state_class": "measurement",
"force_update": True,
},
),
'pv_input_current1': MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Solar Input Current 1",
"unit_of_measurement": "A",
"device_class": "current",
"state_class": "measurement",
"force_update": True,
},
),
'pv_input_power2': MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Solar Input Power 2",
"unit_of_measurement": "W",
"device_class": "power",
"state_class": "measurement",
"force_update": True,
},
),
'pv_input_voltage2': MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Solar Input Voltage 2",
"unit_of_measurement": "V",
"device_class": "voltage",
"state_class": "measurement",
"force_update": True,
},
),
'pv_input_current2': MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Solar Input Current 2",
"unit_of_measurement": "A",
"device_class": "current",
"state_class": "measurement",
"force_update": True,
},
),
"adl400_ac_input_power_phase1": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "ADL400 AC Input Power Phase 1",
"unit_of_measurement": "W",
"device_class": "power",
"state_class": "measurement",
"force_update": True,
},
),
"adl400_ac_input_power_phase2": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "ADL400 AC Input Power Phase 2",
"unit_of_measurement": "W",
"device_class": "power",
"state_class": "measurement",
"force_update": True,
},
),
"adl400_ac_input_power_phase3": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "ADL400 AC Input Power Phase 3",
"unit_of_measurement": "W",
"device_class": "power",
"state_class": "measurement",
"force_update": True,
},
),
"adl400_ac_input_voltage_phase1": MqttFieldConfig( "adl400_ac_input_voltage_phase1": MqttFieldConfig(
type=MqttFieldType.NUMERIC, type=MqttFieldType.NUMERIC,
setter=False, setter=False,
@@ -104,4 +240,312 @@ ADDITIONAL_DEVICE_FIELDS = {
"force_update": True, "force_update": True,
}, },
), ),
"grid_input_voltage_phase1": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Grid Input Voltage Phase 1",
"unit_of_measurement": "V",
"device_class": "voltage",
"state_class": "measurement",
"force_update": True,
},
),
"grid_input_voltage_phase2": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Grid Input Voltage Phase 2",
"unit_of_measurement": "V",
"device_class": "voltage",
"state_class": "measurement",
"force_update": True,
},
),
"grid_input_voltage_phase3": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Grid Input Voltage Phase 3",
"unit_of_measurement": "V",
"device_class": "voltage",
"state_class": "measurement",
"force_update": True,
},
),
"grid_input_current_phase1": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Grid Input Current Phase 1",
"unit_of_measurement": "A",
"device_class": "current",
"state_class": "measurement",
"force_update": True,
},
),
"grid_input_current_phase2": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Grid Input Current Phase 2",
"unit_of_measurement": "A",
"device_class": "current",
"state_class": "measurement",
"force_update": True,
},
),
"grid_input_current_phase3": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Grid Input Current Phase 3",
"unit_of_measurement": "A",
"device_class": "current",
"state_class": "measurement",
"force_update": True,
},
),
"grid_input_power_phase1": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Grid Input Power Phase 1",
"unit_of_measurement": "W",
"device_class": "power",
"state_class": "measurement",
"force_update": True,
},
),
"grid_input_power_phase2": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Grid Input Power Phase 2",
"unit_of_measurement": "W",
"device_class": "power",
"state_class": "measurement",
"force_update": True,
},
),
"grid_input_power_phase3": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Grid Input Power Phase 3",
"unit_of_measurement": "W",
"device_class": "power",
"state_class": "measurement",
"force_update": True,
},
),
"ac_output_frequency": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "AC Output Frequency",
"unit_of_measurement": "Hz",
"device_class": "frequency",
"state_class": "measurement",
"force_update": True,
},
),
"ac_output_voltage_phase1": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "AC Output Voltage Phase 1",
"unit_of_measurement": "V",
"device_class": "voltage",
"state_class": "measurement",
"force_update": True,
},
),
"ac_output_voltage_phase2": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "AC Output Voltage Phase 2",
"unit_of_measurement": "V",
"device_class": "voltage",
"state_class": "measurement",
"force_update": True,
},
),
"ac_output_voltage_phase3": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "AC Output Voltage Phase 3",
"unit_of_measurement": "V",
"device_class": "voltage",
"state_class": "measurement",
"force_update": True,
},
),
"ac_output_power_phase1": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "AC Output Power Phase 1",
"unit_of_measurement": "W",
"device_class": "power",
"state_class": "measurement",
"force_update": True,
},
),
"ac_output_power_phase2": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "AC Output Power Phase 2",
"unit_of_measurement": "W",
"device_class": "power",
"state_class": "measurement",
"force_update": True,
},
),
"ac_output_power_phase3": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "AC Output Power Phase 3",
"unit_of_measurement": "W",
"device_class": "power",
"state_class": "measurement",
"force_update": True,
},
),
"ac_output_current_phase1": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "AC Output Current Phase 1",
"unit_of_measurement": "A",
"device_class": "current",
"state_class": "measurement",
"force_update": True,
},
),
"ac_output_current_phase2": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "AC Output Current Phase 2",
"unit_of_measurement": "A",
"device_class": "current",
"state_class": "measurement",
"force_update": True,
},
),
"ac_output_current_phase3": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "AC Output Current Phase 3",
"unit_of_measurement": "A",
"device_class": "current",
"state_class": "measurement",
"force_update": True,
},
),
"testing0": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Register Testing 0",
},
),
"testing1": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Register Testing 1",
},
),
"testing2": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Register Testing 2",
},
),
"testing3": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Register Testing 3",
},
),
"testing4": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Register Testing 4",
},
),
"testing5": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Register Testing 5",
},
),
"testing6": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Register Testing 6",
},
),
"testing7": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Register Testing 7",
},
),
"testing8": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Register Testing 8",
},
),
"testing9": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Register Testing 9",
},
),
} }
+159 -125
View File
@@ -1,12 +1,11 @@
"""Coordinator for Bluetti integration.""" """Coordinator for Bluetti integration."""
from __future__ import annotations from __future__ import annotations
from typing import List from typing import List, cast
import asyncio import asyncio
from datetime import timedelta from datetime import timedelta
import logging import logging
from typing import cast
import async_timeout import async_timeout
from bleak import BleakClient, BleakError from bleak import BleakClient, BleakError
@@ -22,85 +21,56 @@ from bluetti_mqtt.bluetooth import (
BadConnectionError, BadConnectionError,
ModbusError, ModbusError,
ParseError, ParseError,
build_device,
) )
from bluetti_mqtt.core.devices.bluetti_device import BluettiDevice from bluetti_mqtt.core.devices.bluetti_device import BluettiDevice
from bluetti_mqtt.core.commands import ReadHoldingRegisters from bluetti_mqtt.core.commands import ReadHoldingRegisters
from .const import DATA_POLLING_RUNNING, DOMAIN
from .utils import mac_loggable, build_device
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class DummyDevice(BluettiDevice):
"""Dummy device used to add more fields to existing devices.
Changes made here are only temporary and should also be
contributed to https://github.com/warhammerkid/bluetti_mqtt
"""
def __init__(self, device: BluettiDevice):
"""Init dummy device with real device."""
self.struct = device.struct
if device.type == "EP600":
self.struct.add_decimal_field("adl400_ac_input_voltage_phase1", 1229, 1)
self.struct.add_decimal_field("adl400_ac_input_voltage_phase2", 1237, 1)
self.struct.add_decimal_field("adl400_ac_input_voltage_phase3", 1245, 1)
super().__init__(device.address, device.type, device.sn)
self._parent = device
@property
def polling_commands(self) -> List[ReadHoldingRegisters]:
if self.type == "EP600":
return [
ReadHoldingRegisters(100, 62),
ReadHoldingRegisters(1229, 19),
ReadHoldingRegisters(2022, 6),
ReadHoldingRegisters(2213, 4),
# Battery
ReadHoldingRegisters(6101, 7),
ReadHoldingRegisters(6175, 11),
# current limit for reading ReadHoldingRegisters(0x2F2F, 1),
]
return self._parent.polling_commands
@property
def pack_polling_commands(self) -> List[ReadHoldingRegisters]:
return self._parent.pack_logging_commands
@property
def logging_commands(self) -> List[ReadHoldingRegisters]:
return self._parent.logging_commands
@property
def pack_logging_commands(self) -> List[ReadHoldingRegisters]:
return self._parent.pack_logging_commands
@property
def writable_ranges(self) -> List[range]:
return self._parent.writable_ranges
class PollingCoordinator(DataUpdateCoordinator): class PollingCoordinator(DataUpdateCoordinator):
"""Polling coordinator.""" """Polling coordinator."""
def __init__(self, hass: HomeAssistant, address, device_name: str): def __init__(
self,
hass: HomeAssistant,
address: str,
device_name: str,
polling_interval: int,
persistent_conn: bool,
polling_timeout: int,
max_retries: int,
):
"""Initialize coordinator.""" """Initialize coordinator."""
super().__init__( super().__init__(
hass, hass,
_LOGGER, _LOGGER,
name="Bluetti polling coordinator", name="Bluetti polling coordinator",
update_interval=timedelta(seconds=20), update_interval=timedelta(seconds=polling_interval),
) )
self._address = address self._address = address
self.has_notifier = False
self.notify_future = None self.notify_future = None
self.current_command = None self.current_command = None
self.notify_response = bytearray() self.notify_response = bytearray()
bluetti_device = build_device(address, device_name) self.bluetti_device = build_device(address, device_name)
self.persistent_conn = persistent_conn
self.polling_timeout = polling_timeout
self.max_retries = max_retries
# Add or modify device fields # Create client
self.bluetti_device = DummyDevice(bluetti_device) self.logger.debug("Creating client")
device = bluetooth.async_ble_device_from_address(hass, address)
if device is None:
self.logger.error("Device %s not available", mac_loggable(address))
return None
self.client = BleakClient(device)
# polling mutex to guard against switches
self.polling_lock = asyncio.Lock()
async def _async_update_data(self): async def _async_update_data(self):
"""Fetch data from API endpoint. """Fetch data from API endpoint.
@@ -108,91 +78,155 @@ class PollingCoordinator(DataUpdateCoordinator):
This is the place to pre-process the data to lookup tables This is the place to pre-process the data to lookup tables
so entities can quickly look up their data. so entities can quickly look up their data.
""" """
self.hass.data[DOMAIN][self.config_entry.entry_id][DATA_POLLING_RUNNING] = True
self.logger.debug("Polling data") self.logger.debug("Polling data")
device = bluetooth.async_ble_device_from_address(self.hass, self._address)
if device is None:
self.logger.error("Device %s not available", self._address)
return None
if self.bluetti_device is None: if self.bluetti_device is None:
self.logger.error("Device type for %s not found", self._address) self.logger.error("Device type for %s not found", mac_loggable(self._address))
return None return None
# Polling
client = BleakClient(device)
parsed_data: dict = {} parsed_data: dict = {}
async with self.polling_lock:
try:
async with async_timeout.timeout(self.polling_timeout):
# Reconnect if not connected
for attempt in range(1, self.max_retries + 1):
try:
if not self.client.is_connected:
await self.client.connect()
break
except Exception as e:
if attempt == self.max_retries or attempt == 1:
raise # pass exception on max_retries attempt
else:
self.logger.warning(f"Connect unsucessful (attempt {attempt}): {e}. Retrying...")
await asyncio.sleep(2)
# Attach notifier if needed
if not self.has_notifier:
await self.client.start_notify(
BluetoothClient.NOTIFY_UUID, self._notification_handler
)
self.has_notifier = True
for command in self.bluetti_device.polling_commands:
try:
body = command.parse_response(
await self.async_send_command(command)
)
self.logger.debug("Raw data: %s", body)
parsed = self.bluetti_device.parse(
command.starting_address, body
)
self.logger.debug("Parsed data: %s", parsed)
parsed_data.update(parsed)
except ParseError:
self.logger.warning("Got a parse exception...")
if len(self.bluetti_device.pack_polling_commands) > 0:
self.logger.debug("Polling battery packs")
# pack polling
for pack in range (1, self.bluetti_device.pack_num_max + 1):
# Set current pack number
await self.async_send_command(
self.bluetti_device.build_setter_command('pack_num', pack)
)
# We need to wait after switching packs for the data to be available
await asyncio.sleep(5)
for command in self.bluetti_device.pack_polling_commands:
# Request & parse result for each pack
try:
body = command.parse_response(
await self.async_send_command(command)
)
parsed = self.bluetti_device.parse(
command.starting_address, body
)
self.logger.debug("Parsed data: %s", parsed)
pack_number = parsed.get('pack_num')
if not isinstance(pack_number, int) or pack_number != pack:
self.logger.debug("Parsed pack_num(%s) does not match expected '%s'", pack_number, pack)
continue
for key, value in parsed.items():
parsed_data.update({key+str(pack):value})
except ParseError:
self.logger.warning("Got a parse exception...")
except TimeoutError:
self.logger.warning("Polling timed out for device %s", mac_loggable(self._address))
return None
except BleakError as err:
self.logger.warning("Bleak error: %s", err)
return None
finally:
# Disconnect if connection not persistant
if not self.persistent_conn:
if self.has_notifier:
await self.client.stop_notify(BluetoothClient.NOTIFY_UUID)
self.has_notifier = False
await self.client.disconnect()
self.hass.data[DOMAIN][self.config_entry.entry_id][DATA_POLLING_RUNNING] = False
# Pass data back to sensors
return parsed_data
async def async_send_command(self, command: ReadHoldingRegisters) -> bytes:
"""Send command and return response"""
try: try:
async with async_timeout.timeout(15): # Prepare to make request
await client.connect() self.current_command = command
self.notify_future = self.hass.loop.create_future()
self.notify_response = bytearray()
await client.start_notify( # Make request
BluetoothClient.NOTIFY_UUID, self._notification_handler self.logger.debug("Requesting %s", command)
) await self.client.write_gatt_char(
BluetoothClient.WRITE_UUID, bytes(command)
)
for command in self.bluetti_device.polling_commands: # Wait for response
try: res = await asyncio.wait_for(
# Prepare to make request self.notify_future, timeout=BluetoothClient.RESPONSE_TIMEOUT
self.current_command = command )
self.notify_future = self.hass.loop.create_future()
self.notify_response = bytearray()
# Make request # Process data
self.logger.debug("Requesting %s", command) self.logger.debug("Got %s bytes", len(res))
await client.write_gatt_char( return cast(bytes, res)
BluetoothClient.WRITE_UUID, bytes(command)
)
# Wait for response
res = await asyncio.wait_for(
self.notify_future, timeout=BluetoothClient.RESPONSE_TIMEOUT
)
# Process data
self.logger.debug("Got %s bytes", len(res))
response = cast(bytes, res)
body = command.parse_response(response)
parsed = self.bluetti_device.parse(
command.starting_address, body
)
self.logger.debug("Parsed data: %s", parsed)
parsed_data.update(parsed)
except TimeoutError:
self.logger.error(
"Polling timed out (address: %s)", self._address
)
except ParseError:
self.logger.warning("Got a parse exception...")
except ModbusError as err:
self.logger.warning(
"Got an invalid request error for %s: %s",
command,
err,
)
except (BadConnectionError, BleakError) as err:
self.logger.warning(
"Needed to disconnect due to error: %s", err
)
except TimeoutError: except TimeoutError:
self.logger.error("Polling timed out for device %s", self._address) self.logger.debug(
return None "Polling timed out (address: %s)", mac_loggable(self._address)
except BleakError as err: )
self.logger.error("Bleak error: %s", err) except ModbusError as err:
return None self.logger.warning(
finally: "Got an invalid request error for %s: %s",
await client.disconnect() command,
err,
)
except (BadConnectionError, BleakError) as err:
self.logger.warning(
"Needed to disconnect due to error: %s (This can also be the case if you used device controls)", err
)
# Pass data back to sensors # caught an exception, return empty bytes object
return parsed_data return bytes()
def _notification_handler(self, _sender: int, data: bytearray): def _notification_handler(self, _sender: int, data: bytearray):
"""Handle bt data.""" """Handle bt data."""
# Ignore notifications we don't expect # Ignore notifications we don't expect
if not self.notify_future or self.notify_future.done(): if not self.notify_future or self.notify_future.done():
_LOGGER.warning("Unexpected notification")
return return
# If something went wrong, we might get weird data. # If something went wrong, we might get weird data.
@@ -0,0 +1,113 @@
"""EP600 Field definitions.
Changes made here are only temporary and should also be
contributed to https://github.com/warhammerkid/bluetti_mqtt
"""
from typing import List
from bluetti_mqtt.core.devices.bluetti_device import BluettiDevice
from bluetti_mqtt.core.devices.struct import DeviceStruct
from bluetti_mqtt.core.commands import ReadHoldingRegisters
class EP600(BluettiDevice):
def __init__(self, address: str, sn: str):
self.struct = DeviceStruct()
self.struct.add_uint_field('total_battery_percent', 102)
self.struct.add_swap_string_field('device_type', 110, 6)
self.struct.add_sn_field('serial_number', 116)
self.struct.add_decimal_field('power_generation', 154, 1) # Total power generated since last reset (kwh)
self.struct.add_swap_string_field('device_type', 1101, 6)
self.struct.add_sn_field('serial_number', 1107)
self.struct.add_decimal_field('power_generation', 1202, 1) # Total power generated since last reset (kwh)
self.struct.add_uint_field('battery_range_start', 2022)
self.struct.add_uint_field('battery_range_end', 2023)
self.struct.add_uint_field('max_ac_input_power', 2213)
self.struct.add_uint_field('max_ac_input_current', 2214)
self.struct.add_uint_field('max_ac_output_power', 2215)
self.struct.add_uint_field('max_ac_output_current', 2216)
self.struct.add_swap_string_field('battery_type', 6101, 6)
self.struct.add_sn_field('battery_serial_number', 6107)
self.struct.add_version_field('bcu_version', 6175)
self.struct.add_version_field('bmu_version', 6178)
self.struct.add_version_field('safety_module_version', 6181)
self.struct.add_version_field('high_voltage_module_version', 6184)
# DC Solar Input (copied from PR https://github.com/warhammerkid/bluetti_mqtt/pull/87 by KM011092)
self.struct.add_uint_field('pv_input_power1', 1212) # MPP 1 in - value * 0.1
#self.struct.add_uint_field('pv_input_voltage1', 1213) # MPP 1 in - value * 0.1
#self.struct.add_uint_field('pv_input_current1', 1214) # MPP 1 in
self.struct.add_uint_field('pv_input_power2', 1220) # MPP 2 in - value * 0.1
#self.struct.add_uint_field('pv_input_voltage2', 1221) # MPP 2 in - value * 0.1
#self.struct.add_uint_field('pv_input_current2', 1222) # MPP 2 in
# ADL400 Smart Meter for AC Solar
self.struct.add_uint_field("adl400_ac_input_power_phase1", 1228)
self.struct.add_uint_field("adl400_ac_input_power_phase2", 1236)
self.struct.add_uint_field("adl400_ac_input_power_phase3", 1244)
self.struct.add_decimal_field("adl400_ac_input_voltage_phase1", 1229, 1)
self.struct.add_decimal_field("adl400_ac_input_voltage_phase2", 1237, 1)
self.struct.add_decimal_field("adl400_ac_input_voltage_phase3", 1245, 1)
# Grid Input
self.struct.add_decimal_field("grid_input_frequency", 1300, 1)
self.struct.add_uint_field("grid_input_power_phase1", 1313)
self.struct.add_uint_field("grid_input_power_phase2", 1319)
self.struct.add_uint_field("grid_input_power_phase3", 1325)
self.struct.add_decimal_field("grid_input_voltage_phase1", 1314, 1)
self.struct.add_decimal_field("grid_input_voltage_phase2", 1320, 1)
self.struct.add_decimal_field("grid_input_voltage_phase3", 1326, 1)
self.struct.add_decimal_field("grid_input_current_phase1", 1315, 1)
self.struct.add_decimal_field("grid_input_current_phase2", 1321, 1)
self.struct.add_decimal_field("grid_input_current_phase3", 1327, 1)
# EP600 AC Output
self.struct.add_decimal_field("ac_output_frequency", 1500, 1)
self.struct.add_uint_field("ac_output_power_phase1", 1510)
self.struct.add_uint_field("ac_output_power_phase2", 1517)
self.struct.add_uint_field("ac_output_power_phase3", 1524)
self.struct.add_decimal_field("ac_output_voltage_phase1", 1511, 1)
self.struct.add_decimal_field("ac_output_voltage_phase2", 1518, 1)
self.struct.add_decimal_field("ac_output_voltage_phase3", 1525, 1)
self.struct.add_decimal_field("ac_output_current_phase1", 1512, 1)
self.struct.add_decimal_field("ac_output_current_phase2", 1519, 1)
self.struct.add_decimal_field("ac_output_current_phase3", 1526, 1)
super().__init__(address, 'EP600', sn)
@property
def polling_commands(self) -> List[ReadHoldingRegisters]:
return [
ReadHoldingRegisters(100, 62),
ReadHoldingRegisters(1228, 19),
ReadHoldingRegisters(1300, 28),
ReadHoldingRegisters(1500, 27),
ReadHoldingRegisters(2022, 6),
ReadHoldingRegisters(2213, 4),
ReadHoldingRegisters(1212, 11),
# Battery
ReadHoldingRegisters(6101, 7),
ReadHoldingRegisters(6175, 11),
]
@property
def pack_polling_commands(self) -> List[ReadHoldingRegisters]:
return []
@property
def logging_commands(self) -> List[ReadHoldingRegisters]:
return [
ReadHoldingRegisters(100, 62),
ReadHoldingRegisters(1100, 51),
ReadHoldingRegisters(1200, 90),
ReadHoldingRegisters(1300, 31),
ReadHoldingRegisters(1400, 48),
ReadHoldingRegisters(1500, 30),
ReadHoldingRegisters(2000, 89),
ReadHoldingRegisters(2200, 41),
ReadHoldingRegisters(2300, 36),
ReadHoldingRegisters(6000, 32),
ReadHoldingRegisters(6100, 100),
ReadHoldingRegisters(6300, 100),
]
@@ -0,0 +1,67 @@
"""EP760 Field definitions.
Changes made here are only temporary and should also be
contributed to https://github.com/warhammerkid/bluetti_mqtt
"""
from typing import List
from bluetti_mqtt.core.devices.bluetti_device import BluettiDevice
from bluetti_mqtt.core.devices.struct import DeviceStruct
from bluetti_mqtt.core.commands import ReadHoldingRegisters
class EP760(BluettiDevice):
def __init__(self, address: str, sn: str):
self.struct = DeviceStruct()
# TODO has to be tested
self.struct.add_uint_field('total_battery_percent', 102)
self.struct.add_swap_string_field('device_type', 110, 6)
self.struct.add_sn_field('serial_number', 116)
self.struct.add_decimal_field('power_generation', 154, 1)
self.struct.add_swap_string_field('device_type', 1101, 6)
self.struct.add_sn_field('serial_number', 1107)
self.struct.add_decimal_field('power_generation', 1202, 1)
self.struct.add_uint_field('battery_range_start', 2022)
self.struct.add_uint_field('battery_range_end', 2023)
self.struct.add_uint_field('max_ac_input_power', 2213)
self.struct.add_uint_field('max_ac_input_current', 2214)
self.struct.add_uint_field('max_ac_output_power', 2215)
self.struct.add_uint_field('max_ac_output_current', 2216)
self.struct.add_swap_string_field('battery_type', 6101, 6)
self.struct.add_sn_field('battery_serial_number', 6107)
self.struct.add_version_field('bcu_version', 6175)
self.struct.add_version_field('bmu_version', 6178)
self.struct.add_version_field('safety_module_version', 6181)
self.struct.add_version_field('high_voltage_module_version', 6184)
super().__init__(address, 'EP760', sn)
@property
def polling_commands(self) -> List[ReadHoldingRegisters]:
return [
ReadHoldingRegisters(100, 62),
ReadHoldingRegisters(2022, 2),
]
@property
def pack_polling_commands(self) -> List[ReadHoldingRegisters]:
return []
@property
def logging_commands(self) -> List[ReadHoldingRegisters]:
return [
ReadHoldingRegisters(100, 62),
ReadHoldingRegisters(1100, 51),
ReadHoldingRegisters(1200, 90),
ReadHoldingRegisters(1300, 31),
ReadHoldingRegisters(1400, 48),
ReadHoldingRegisters(1500, 30),
ReadHoldingRegisters(2000, 89),
ReadHoldingRegisters(2200, 41),
ReadHoldingRegisters(2300, 36),
ReadHoldingRegisters(6000, 32),
ReadHoldingRegisters(6100, 100),
ReadHoldingRegisters(6300, 100),
]
+4 -2
View File
@@ -8,7 +8,9 @@
{ "local_name": "AC60*" }, { "local_name": "AC60*" },
{ "local_name": "EB3A*" }, { "local_name": "EB3A*" },
{ "local_name": "EP500*" }, { "local_name": "EP500*" },
{ "local_name": "EP600*" } { "local_name": "EP600*" },
{ "local_name": "EP760*" },
{ "local_name": "PBOX*" }
], ],
"codeowners": ["@Patrick762"], "codeowners": ["@Patrick762"],
"config_flow": true, "config_flow": true,
@@ -18,5 +20,5 @@
"iot_class": "local_polling", "iot_class": "local_polling",
"issue_tracker": "https://github.com/Patrick762/hassio-bluetti-bt/issues", "issue_tracker": "https://github.com/Patrick762/hassio-bluetti-bt/issues",
"requirements": ["bluetti_mqtt==0.15.0"], "requirements": ["bluetti_mqtt==0.15.0"],
"version": "0.0.2" "version": "0.0.16"
} }
+38 -15
View File
@@ -1,6 +1,7 @@
"""Bluetti BT sensors.""" """Bluetti BT sensors."""
from __future__ import annotations from __future__ import annotations
from enum import Enum
import logging import logging
from decimal import Decimal from decimal import Decimal
@@ -21,16 +22,17 @@ from homeassistant.helpers.update_coordinator import (
CoordinatorEntity, CoordinatorEntity,
) )
from bluetti_mqtt.bluetooth import build_device
from bluetti_mqtt.mqtt_client import ( from bluetti_mqtt.mqtt_client import (
NORMAL_DEVICE_FIELDS, NORMAL_DEVICE_FIELDS,
DC_INPUT_FIELDS, DC_INPUT_FIELDS,
MqttFieldType, MqttFieldType,
battery_pack_fields,
) )
from . import device_info as dev_info, get_unique_id from . import device_info as dev_info, get_unique_id
from .const import DOMAIN, CONF_OPTIONS, DIAGNOSTIC_FIELDS, ADDITIONAL_DEVICE_FIELDS from .const import DATA_COORDINATOR, DOMAIN, CONF_OPTIONS, DIAGNOSTIC_FIELDS, ADDITIONAL_DEVICE_FIELDS
from .coordinator import PollingCoordinator, DummyDevice from .coordinator import PollingCoordinator
from .utils import unique_id_loggable, build_device
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -51,21 +53,28 @@ async def async_setup_entry(
# Add sensors according to device_info # Add sensors according to device_info
bluetti_device = build_device(address, device_name) bluetti_device = build_device(address, device_name)
bluetti_device = DummyDevice(bluetti_device)
sensors_to_add = [] sensors_to_add = []
all_fields = NORMAL_DEVICE_FIELDS all_fields = NORMAL_DEVICE_FIELDS
all_fields.update(DC_INPUT_FIELDS) all_fields.update(DC_INPUT_FIELDS)
all_fields.update(ADDITIONAL_DEVICE_FIELDS) all_fields.update(ADDITIONAL_DEVICE_FIELDS)
if len(bluetti_device.pack_polling_commands) > 0:
# add pack fields for device
_LOGGER.info("Device type(%s) pack_num_max(%s)", bluetti_device.type, bluetti_device.pack_num_max)
for pack in range (1, bluetti_device.pack_num_max + 1):
for name, field in battery_pack_fields(pack).items():
all_fields.update({name+str(pack): field})
for field_key, field_config in all_fields.items(): for field_key, field_config in all_fields.items():
if bluetti_device.has_field(field_key): if bluetti_device.has_field(field_key) or field_config.id_override is not None:
category = None category = None
if field_config.setter is True or field_key in DIAGNOSTIC_FIELDS: if field_config.setter is True or field_key in DIAGNOSTIC_FIELDS:
category = EntityCategory.DIAGNOSTIC category = EntityCategory.DIAGNOSTIC
if field_config.type == MqttFieldType.NUMERIC: if field_config.type == MqttFieldType.NUMERIC:
sensors_to_add.append( sensors_to_add.append(
BluettiSensor( BluettiSensor(
hass.data[DOMAIN][entry.entry_id], hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR],
device_info, device_info,
address, address,
field_key, field_key,
@@ -79,7 +88,7 @@ async def async_setup_entry(
elif field_config.type == MqttFieldType.ENUM: elif field_config.type == MqttFieldType.ENUM:
sensors_to_add.append( sensors_to_add.append(
BluettiSensor( BluettiSensor(
hass.data[DOMAIN][entry.entry_id], hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR],
device_info, device_info,
address, address,
field_key, field_key,
@@ -111,12 +120,13 @@ class BluettiSensor(CoordinatorEntity, SensorEntity):
"""Init battery entity.""" """Init battery entity."""
super().__init__(coordinator) super().__init__(coordinator)
self._attr_has_entity_name = True
e_name = f"{device_info.get('name')} {name}" e_name = f"{device_info.get('name')} {name}"
self._address = address self._address = address
self._response_key = response_key self._response_key = response_key
self._attr_device_info = device_info self._attr_device_info = device_info
self._attr_name = e_name self._attr_name = name
self._attr_available = False self._attr_available = False
self._attr_unique_id = get_unique_id(e_name) self._attr_unique_id = get_unique_id(e_name)
self._attr_native_unit_of_measurement = unit_of_measurement self._attr_native_unit_of_measurement = unit_of_measurement
@@ -128,16 +138,28 @@ class BluettiSensor(CoordinatorEntity, SensorEntity):
@callback @callback
def _handle_coordinator_update(self) -> None: def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator.""" """Handle updated data from the coordinator."""
_LOGGER.debug("Updating state of %s", self._attr_unique_id)
if self.coordinator.persistent_conn and not self.coordinator.client.is_connected:
return
if self.coordinator.data is None:
_LOGGER.warning(
"Data from coordinator is None. Skipping update for %s",
unique_id_loggable(self._attr_unique_id)
)
return
_LOGGER.debug("Updating state of %s", unique_id_loggable(self._attr_unique_id))
if not isinstance(self.coordinator.data, dict): if not isinstance(self.coordinator.data, dict):
_LOGGER.error( _LOGGER.warning(
"Invalid data from coordinator (sensor.%s)", self._attr_unique_id "Invalid data from coordinator (sensor.%s)", unique_id_loggable(self._attr_unique_id)
) )
self._attr_available = False self._attr_available = False
return return
response_data = self.coordinator.data.get(self._response_key) response_data = self.coordinator.data.get(self._response_key)
if response_data is None: if response_data is None:
_LOGGER.warning("No data for available for (%s)", self._response_key)
self._attr_available = False self._attr_available = False
return return
@@ -146,10 +168,11 @@ class BluettiSensor(CoordinatorEntity, SensorEntity):
and not isinstance(response_data, float) and not isinstance(response_data, float)
and not isinstance(response_data, complex) and not isinstance(response_data, complex)
and not isinstance(response_data, Decimal) and not isinstance(response_data, Decimal)
and not isinstance(response_data, Enum)
): ):
_LOGGER.warning( _LOGGER.warning(
"Invalid response data type from coordinator (sensor.%s): %s has type %s", "Invalid response data type from coordinator (sensor.%s): %s has type %s",
self._attr_unique_id, unique_id_loggable(self._attr_unique_id),
response_data, response_data,
type(response_data), type(response_data),
) )
@@ -161,11 +184,11 @@ class BluettiSensor(CoordinatorEntity, SensorEntity):
# Different for enum and numeric # Different for enum and numeric
if ( if (
self._options is not None self._options is not None
and isinstance(response_data, int) and isinstance(response_data, Enum)
and response_data < len(self._options) and response_data.value < len(self._options)
): ):
# Enum # Enum
self._attr_native_value = self._options[response_data] self._attr_native_value = self._options[response_data.value]
else: else:
# Numeric # Numeric
self._attr_native_value = response_data self._attr_native_value = response_data
+189
View File
@@ -0,0 +1,189 @@
"""Bluetti BT switches."""
from __future__ import annotations
import asyncio
import logging
import async_timeout
from bleak import BleakError
from homeassistant.components import bluetooth
from homeassistant.components.switch import SwitchEntity, SwitchDeviceClass
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.const import (
CONF_ADDRESS,
CONF_NAME,
EntityCategory,
)
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
)
from bluetti_mqtt.bluetooth.client import BluetoothClient
from bluetti_mqtt.core.devices.bluetti_device import BluettiDevice
from bluetti_mqtt.mqtt_client import (
NORMAL_DEVICE_FIELDS,
DC_INPUT_FIELDS,
MqttFieldType,
)
from . import device_info as dev_info, get_unique_id
from .const import CONTROL_FIELDS, DATA_COORDINATOR, DOMAIN, ADDITIONAL_DEVICE_FIELDS
from .coordinator import PollingCoordinator
from .utils import mac_loggable, unique_id_loggable, build_device
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Setup switch entities."""
device_name = entry.data.get(CONF_NAME)
address = entry.data.get(CONF_ADDRESS)
if address is None:
_LOGGER.error("Device has no address")
# Generate device info
_LOGGER.info("Creating switches for device with address %s", address)
device_info = dev_info(entry)
# Add sensors according to device_info
bluetti_device = build_device(address, device_name)
sensors_to_add = []
all_fields = NORMAL_DEVICE_FIELDS
all_fields.update(DC_INPUT_FIELDS)
all_fields.update(ADDITIONAL_DEVICE_FIELDS)
for field_key, field_config in all_fields.items():
if bluetti_device.has_field(field_key):
if field_config.type == MqttFieldType.BOOL:
if field_config.setter is True and field_key in CONTROL_FIELDS:
sensors_to_add.append(
BluettiSwitch(
bluetti_device,
hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR],
device_info,
address,
field_key,
field_config.home_assistant_extra.get(CONF_NAME, ""),
entry.entry_id
)
)
async_add_entities(sensors_to_add)
class BluettiSwitch(CoordinatorEntity, SwitchEntity):
"""Bluetti universal switch."""
def __init__(
self,
bluetti_device: BluettiDevice,
coordinator: PollingCoordinator,
device_info: DeviceInfo,
address,
response_key: str,
name: str,
entry_id: str,
category: EntityCategory | None = None,
):
"""Init entity."""
super().__init__(coordinator)
self._bluetti_device = bluetti_device
self._coordinator = coordinator
self._client = coordinator.client
self._polling_lock = coordinator.polling_lock
e_name = f"{device_info.get('name')} {name}"
self._address = address
self._response_key = response_key
self._entry_id = entry_id
self._attr_device_info = device_info
self._attr_has_entity_name = True
self._attr_name = name
self._attr_available = False
self._attr_unique_id = get_unique_id(e_name)
self._attr_entity_category = category
self._attr_device_class = SwitchDeviceClass.OUTLET
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
if self.coordinator.persistent_conn and not self.coordinator.client.is_connected:
return
_LOGGER.debug("Updating state of %s", unique_id_loggable(self._attr_unique_id))
if not isinstance(self.coordinator.data, dict):
_LOGGER.error(
"Invalid data from coordinator (switch.%s)", unique_id_loggable(self._attr_unique_id)
)
self._attr_available = False
return
response_data = self.coordinator.data.get(self._response_key)
if response_data is None:
self._attr_available = False
return
if not isinstance(response_data, bool):
_LOGGER.warning(
"Invalid response data type from coordinator (switch.%s): %s",
unique_id_loggable(self._attr_unique_id),
response_data,
)
self._attr_available = False
return
self._attr_available = True
self._attr_is_on = self.coordinator.data[self._response_key] is True
self.async_write_ha_state()
async def async_turn_on(self, **kwargs):
"""Turn the entity on."""
_LOGGER.debug("Turn on %s on %s", self._response_key, mac_loggable(self._address))
await self.write_to_device(True)
async def async_turn_off(self, **kwargs):
"""Turn the entity off."""
_LOGGER.debug("Turn off %s on %s", self._response_key, mac_loggable(self._address))
await self.write_to_device(False)
async def write_to_device(self, state: bool):
"""Write to device."""
command = self._bluetti_device.build_setter_command(self._response_key, state)
async with self._polling_lock:
try:
async with async_timeout.timeout(15):
if not self._client.is_connected:
await self._client.connect()
# Send command
_LOGGER.debug("Requesting %s (%s,%s)", command, self._response_key, state)
await self._client.write_gatt_char(
BluetoothClient.WRITE_UUID, bytes(command)
)
# Wait until device has changed value, otherwise reading register might reset it
await asyncio.sleep(5)
except TimeoutError:
_LOGGER.error("Timed out for device %s", mac_loggable(self._address))
return None
except BleakError as err:
_LOGGER.error("Bleak error: %s", err)
return None
finally:
# Disconnect if connection not persistant
if not self._coordinator.persistent_conn:
await self._client.disconnect()
await self.coordinator.async_request_refresh()
@@ -0,0 +1,30 @@
{
"config": {
"step": {
"user": {
"data": {
"address": "Gerät",
"type": "Modell (erforderlich bei PBOX Geräten)"
}
}
}
},
"options": {
"step": {
"init": {
"data": {
"use_controls": "Steuerung aktivieren (auf eigenes Risiko)",
"persistent_conn": "Dauerhafte Verbindung (Neustart erforderlich)",
"polling_interval": "Datenabruf-Intervall in Sekunden (Neustart erforderlich)",
"polling_timeout": "Datenabruf-Timeout in Sekunden (Neustart erforderlich)",
"max_retries": "Maximale Verbindungsversuche (Neustart erforderlich)"
}
}
},
"abort": {
"invalid_interval": "Ungültiger Datenabruf-Intervall. Verwende 5 Sekunden oder mehr",
"invalid_timeout": "Ungültiger Datenabruf-Timeout. Verwende 1 Sekunde oder mehr",
"invalid_retries": "Ungültige maximale Verbindungsversuche. Verwende 1 oder mehr"
}
}
}
@@ -0,0 +1,30 @@
{
"config": {
"step": {
"user": {
"data": {
"address": "Device",
"type": "Model (required for PBOX devices)"
}
}
}
},
"options": {
"step": {
"init": {
"data": {
"use_controls": "Use controls (use at own risk)",
"persistent_conn": "Persistent connection (restart required)",
"polling_interval": "Polling interval in seconds (restart required)",
"polling_timeout": "Polling timeout in seconds (restart required)",
"max_retries": "Maximum amount of connection retries (restart required)"
}
}
},
"abort": {
"invalid_interval": "Invalid polling interval. Use 5 seconds or more",
"invalid_timeout": "Invalid polling timeout. Use 1 second or more",
"invalid_retries": "Invalid max retries. Use 1 or more"
}
}
}
@@ -0,0 +1,16 @@
{
"options": {
"step": {
"init": {
"data": {
"use_controls": "Usar os comandos",
"persistent_conn": "Ligação presistente (necessario reninciar)",
"polling_interval": "Intervalo deatualização (necessario reninciar)"
}
}
},
"abort": {
"invalid_interval": "Valor invalido . Tem de ser no minimo 5 segundos"
}
}
}
+27
View File
@@ -0,0 +1,27 @@
"""Tools."""
from bluetti_mqtt.bluetooth import build_device as orig_build_device
from .const import DEVICE_NAME_RE
from .devices.ep600 import EP600
from .devices.ep760 import EP760
def mac_loggable(mac: str) -> str:
"""Remove parts of the mac address for logging."""
splitted = mac.split(":")
return "XX:XX:XX:XX:XX:" + splitted[-1]
def unique_id_loggable(unique_id: str) -> str:
"""Remove parts of the unique id for logging."""
splitted = unique_id.split("_", maxsplit=1)
serial = splitted[0][:6]
return serial + "XXXXXXXXXX" + splitted[1]
def build_device(address: str, name: str):
match = DEVICE_NAME_RE.match(name)
if match[1] == 'EP760':
return EP760(address, match[2])
elif match[1] == 'EP600':
return EP600(address, match[2])
else:
return orig_build_device(address, name)
-1
View File
@@ -1 +0,0 @@
robocopy .\custom_components\bluetti_bt T:\custom_components\bluetti_bt /E
Executable
+5
View File
@@ -0,0 +1,5 @@
#!/bin/sh
# This script is only for development purposes
sudo cp -r ./custom_components/* /var/ha_config/custom_components/