Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for Victron BLE devices #128843

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -1597,6 +1597,8 @@ build.json @home-assistant/supervisor
/tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja
/homeassistant/components/vicare/ @CFenner
/tests/components/vicare/ @CFenner
/homeassistant/components/victron_ble/ @rajlaud
/tests/components/victron_ble/ @rajlaud
/homeassistant/components/vilfo/ @ManneW
/tests/components/vilfo/ @ManneW
/homeassistant/components/vivotek/ @HarlemSquirrel
Expand Down
66 changes: 66 additions & 0 deletions homeassistant/components/victron_ble/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""The Victron Bluetooth Low Energy integration."""

from __future__ import annotations

from dataclasses import dataclass
import logging

from victron_ble_ha_parser import VictronBluetoothDeviceData

from homeassistant.components.bluetooth import (
BluetoothScanningMode,
async_rediscover_address,
)
from homeassistant.components.bluetooth.passive_update_processor import (
PassiveBluetoothProcessorCoordinator,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN, Platform
from homeassistant.core import HomeAssistant

_LOGGER = logging.getLogger(__name__)


type VictronBLEConfigEntry = ConfigEntry[VictronBLEData]


@dataclass
class VictronBLEData:
"""Class to hold data for the Victron BLE device."""

coordinator: PassiveBluetoothProcessorCoordinator | None = None


async def async_setup_entry(hass: HomeAssistant, entry: VictronBLEConfigEntry) -> bool:
"""Set up Victron BLE device from a config entry."""
address = entry.unique_id
assert address is not None
key = entry.data[CONF_ACCESS_TOKEN]
data = VictronBluetoothDeviceData(key)
coordinator = PassiveBluetoothProcessorCoordinator(
hass,
_LOGGER,
address=address,
mode=BluetoothScanningMode.ACTIVE,
update_method=data.update,
)
entry.runtime_data = VictronBLEData(coordinator)

await hass.config_entries.async_forward_entry_setups(entry, [Platform.SENSOR])
entry.async_on_unload(coordinator.async_start())

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = False

unload_ok = await hass.config_entries.async_forward_entry_unload(
entry, Platform.SENSOR
)

if unload_ok:
async_rediscover_address(hass, entry.entry_id)

return unload_ok
125 changes: 125 additions & 0 deletions homeassistant/components/victron_ble/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"""Config flow for Victron Bluetooth Low Energy integration."""

from __future__ import annotations

import logging
from typing import Any

from victron_ble_ha_parser import VictronBluetoothDeviceData
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.components.bluetooth import (
BluetoothServiceInfoBleak,
async_discovered_service_info,
)
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_ADDRESS

from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)

STEP_ACCESS_TOKEN_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_ACCESS_TOKEN): str,
}
)


class VictronBLEConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Victron Bluetooth Low Energy."""

VERSION = 1

def __init__(self) -> None:
"""Initialize the config flow."""
self._discovered_device: str | None = None
self._discovered_devices: dict[str, str] = {}
self._discovered_devices_info: dict[str, BluetoothServiceInfoBleak] = {}

async def async_step_bluetooth(
self, discovery_info: BluetoothServiceInfoBleak
) -> config_entries.ConfigFlowResult:
"""Handle the bluetooth discovery step."""
_LOGGER.debug("async_step_bluetooth: %s", discovery_info.address)
await self.async_set_unique_id(discovery_info.address)
self._abort_if_unique_id_configured()
device = VictronBluetoothDeviceData()
if not device.supported(discovery_info):
_LOGGER.debug("device %s not supported", discovery_info.address)
return self.async_abort(reason="not_supported")

self._discovered_device = discovery_info.address
self._discovered_devices_info[discovery_info.address] = discovery_info
self._discovered_devices[discovery_info.address] = discovery_info.name

self.context["title_placeholders"] = {"title": discovery_info.name}

return await self.async_step_access_token()

async def async_step_access_token(
self, user_input: dict[str, Any] | None = None
) -> config_entries.ConfigFlowResult:
"""Handle advertisement key input."""
# should only be called if there are discovered devices
assert self._discovered_device is not None
assert self._discovered_devices_info is not None
discovery_info = self._discovered_devices_info[self._discovered_device]
assert discovery_info is not None
title = discovery_info.name

if user_input is not None:
# see if we can create a device with the access token
device = VictronBluetoothDeviceData(user_input[CONF_ACCESS_TOKEN])
if device.validate_advertisement_key(
discovery_info.manufacturer_data[0x02E1]
):
return self.async_create_entry(
title=title,
data=user_input,
)
return self.async_abort(reason="invalid_access_token")

return self.async_show_form(
step_id="access_token",
data_schema=STEP_ACCESS_TOKEN_DATA_SCHEMA,
description_placeholders={"title": title},
)

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> config_entries.ConfigFlowResult:
"""Handle select a device to set up."""
if user_input is not None:
address = user_input[CONF_ADDRESS]
await self.async_set_unique_id(address, raise_on_progress=False)
self._abort_if_unique_id_configured()
self._discovered_device = address
title = self._discovered_devices_info[address].name
return self.async_show_form(
step_id="access_token",
data_schema=STEP_ACCESS_TOKEN_DATA_SCHEMA,
description_placeholders={"title": title},
)

current_addresses = self._async_current_ids()
for discovery_info in async_discovered_service_info(self.hass, False):
address = discovery_info.address
if address in current_addresses or address in self._discovered_devices:
continue
device = VictronBluetoothDeviceData()
if device.supported(discovery_info):
self._discovered_devices_info[address] = discovery_info
self._discovered_devices[address] = discovery_info.name

if len(self._discovered_devices) < 1:
return self.async_abort(reason="no_devices_found")

_LOGGER.debug("Discovered %s devices", len(self._discovered_devices))

return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)}
),
)
4 changes: 4 additions & 0 deletions homeassistant/components/victron_ble/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""Constants for the Victron Bluetooth Low Energy integration."""

DOMAIN = "victron_ble"
VICTRON_IDENTIFIER = 0x02E1
18 changes: 18 additions & 0 deletions homeassistant/components/victron_ble/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"domain": "victron_ble",
"name": "Victron Bluetooth Low Energy",
"bluetooth": [
{
"connectable": false,
"manufacturer_id": 737,
"manufacturer_data_start": [16]
}
],
"codeowners": ["@rajlaud"],
"config_flow": true,
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/victron_ble",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["victron-ble-ha-parser==0.4.4"]
}
Loading
Loading