Initial commit
Basic functionality could work (untested!)
This commit is contained in:
8
CONTRIBUTING.md
Normal file
8
CONTRIBUTING.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Contributing
|
||||
|
||||
## Issues
|
||||
In case you have troubles, please enable the debug protocol for the integration and [open an issue](https://github.com/patman15/hdpv_ble/issues) with a good description of what happened and the relevant snippet from the log.
|
||||
|
||||
### Any contributions you make will be under the Apache License Version 2.0
|
||||
|
||||
In short, when you submit code changes, your submissions are understood to be under the same [Apache License Version 2.0](LICENSE) that covers the project. Feel free to contact the maintainers if that's a concern.
|
||||
@@ -1,2 +1,3 @@
|
||||
# hunterdouglas_powerview_ble
|
||||
# HD PowerView Support via BLE for Home Assistant
|
||||
A Home Assistant integration to support Hunter Douglas Powerview devices via Bluetooth
|
||||
|
||||
|
||||
1
custom_components/__init__.py
Normal file
1
custom_components/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Custom component."""
|
||||
65
custom_components/hunterdouglas_powerview_ble/__init__.py
Normal file
65
custom_components/hunterdouglas_powerview_ble/__init__.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""The Hunter Douglas PowerView (BLE) integration."""
|
||||
|
||||
from homeassistant.components.bluetooth import async_ble_device_from_address
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
from .coordinator import PVCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.COVER]
|
||||
|
||||
type ConfigEntryType = ConfigEntry[PVCoordinator]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntryType) -> bool:
|
||||
"""Set up BT Battery Management System from a config entry."""
|
||||
LOGGER.debug("Setup of %s", repr(entry))
|
||||
|
||||
if entry.unique_id is None:
|
||||
raise ConfigEntryError("Missing unique ID for device.")
|
||||
|
||||
ble_device = async_ble_device_from_address(
|
||||
hass=hass, address=entry.unique_id, connectable=True
|
||||
)
|
||||
|
||||
if not ble_device:
|
||||
raise ConfigEntryNotReady(
|
||||
f"Could not find PowerView device ({entry.unique_id}) via Bluetooth"
|
||||
)
|
||||
|
||||
coordinator = PVCoordinator(hass, ble_device)
|
||||
# Query the device the first time, initialise coordinator.data
|
||||
try:
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
except ConfigEntryNotReady:
|
||||
# Ignore, e.g. timeouts, to gracefully handle connection issues
|
||||
LOGGER.warning("Failed to initialize PowerView device %s, continuing", ble_device.name)
|
||||
|
||||
# Insert the coordinator in the global registry
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
entry.runtime_data = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntryType) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
LOGGER.debug("Unloaded config entry: %s, ok? %s!", entry.unique_id, str(unload_ok))
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntryType) -> bool:
|
||||
"""Migrate old entry."""
|
||||
|
||||
if config_entry.version > 1:
|
||||
# This means the user has downgraded from a future version
|
||||
LOGGER.debug("Cannot downgrade from version %s", config_entry.version)
|
||||
return False
|
||||
|
||||
LOGGER.debug("Migrating from version %s", config_entry.version)
|
||||
|
||||
return False
|
||||
128
custom_components/hunterdouglas_powerview_ble/api.py
Normal file
128
custom_components/hunterdouglas_powerview_ble/api.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""Hunter Douglas PowerView BLE API."""
|
||||
|
||||
import asyncio
|
||||
from enum import Enum
|
||||
|
||||
from bleak import BleakClient
|
||||
from bleak.backends.device import BLEDevice
|
||||
from bleak.exc import BleakError
|
||||
|
||||
from .const import LOGGER
|
||||
|
||||
|
||||
class shade_cmd(Enum):
|
||||
"""The PowerView cover commands."""
|
||||
|
||||
set_position = 0x01F7
|
||||
stop = 0xB8F7
|
||||
activate_scene = 0xBAF7
|
||||
|
||||
|
||||
class PowerViewBLE:
|
||||
"""Class to handle connection to PowerView remote device."""
|
||||
|
||||
UUID_SERVICE = "0000fdc1-0000-1000-8000-00805f9b34fb"
|
||||
UUID_TX = "cafe1001-c0ff-ee01-8000-A110CA7AB1E0"
|
||||
|
||||
def __init__(self, ble_device: BLEDevice) -> None:
|
||||
"""Initialize device API via Bluetooth."""
|
||||
self._ble_device: BLEDevice = ble_device
|
||||
self.name = self._ble_device.name
|
||||
self.seqcnt: int = 0
|
||||
self._client: BleakClient | None = None
|
||||
self._data_event = asyncio.Event()
|
||||
|
||||
async def _wait_event(self) -> None:
|
||||
await self._data_event.wait()
|
||||
self._data_event.clear()
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
"""Return whether remote device is connected."""
|
||||
return self._client is not None and self._client.is_connected
|
||||
|
||||
# general cmd: uint16_t cmd, uint8_t seqID, uint8_t data_len
|
||||
async def _cmd(self, cmd: shade_cmd, data: bytearray) -> None:
|
||||
try:
|
||||
await self._connect()
|
||||
assert self._client is not None, "missing BT client"
|
||||
tx_data = (
|
||||
bytearray(
|
||||
int.to_bytes(cmd.value, 2, byteorder="little")
|
||||
+ bytes([self.seqcnt, len(data)])
|
||||
)
|
||||
+ data
|
||||
)
|
||||
self.seqcnt += 1
|
||||
await self._client.write_gatt_char(self.UUID_TX, tx_data, False)
|
||||
finally:
|
||||
await self._disconnect()
|
||||
|
||||
# position cmd: uint16_t pos1, uint16_t pos2, uint16_t pos3, uint16_t tilt, uint8_t velocity
|
||||
async def set_position(self, value: int) -> None:
|
||||
"""Set position of device."""
|
||||
LOGGER.debug("%s setting position to %i", self.name, value)
|
||||
await self._cmd(
|
||||
shade_cmd.set_position,
|
||||
bytearray(
|
||||
int.to_bytes(value * 100, 2, byteorder="little")
|
||||
+ bytes([0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x0])
|
||||
),
|
||||
)
|
||||
|
||||
# TODO: currently HA / Bleak communication is too slow to make that function reasonable
|
||||
# def stop(self) -> None:
|
||||
# """Stop device movement."""
|
||||
# LOGGER.debug("%s stop")
|
||||
|
||||
# uint8_t scene#, uint8_t unknown
|
||||
# open: scene 2
|
||||
# close: scene 3
|
||||
async def activate_scene(self, idx: int) -> None:
|
||||
"""Stop device movement."""
|
||||
LOGGER.debug("%s set scene #%i", self.name, idx)
|
||||
await self._cmd(
|
||||
shade_cmd.activate_scene,
|
||||
bytearray(
|
||||
int.to_bytes(idx, 1, byteorder="little")
|
||||
+ bytes([0xA2])
|
||||
),
|
||||
)
|
||||
|
||||
def _on_disconnect(self, client: BleakClient) -> None:
|
||||
"""Disconnect callback function."""
|
||||
|
||||
LOGGER.debug("Disconnected from %s", client.address)
|
||||
|
||||
def _notification_handler(self, sender, data: bytearray) -> None:
|
||||
LOGGER.debug("%s received BLE data: %s", self.name, data)
|
||||
|
||||
self._data_event.set()
|
||||
|
||||
async def _connect(self) -> None:
|
||||
"""Connect to the device and setup notification if not connected."""
|
||||
|
||||
if not self.is_connected:
|
||||
LOGGER.debug("Connecting %s", self.name)
|
||||
self._client = BleakClient(
|
||||
self._ble_device,
|
||||
disconnected_callback=self._on_disconnect,
|
||||
services=[self.UUID_SERVICE],
|
||||
)
|
||||
await self._client.connect()
|
||||
await self._client.start_notify(self.UUID_TX, self._notification_handler)
|
||||
else:
|
||||
LOGGER.debug("%s already connected", self.name)
|
||||
|
||||
async def _disconnect(self) -> None:
|
||||
"""Disconnect the device and stop notifications."""
|
||||
|
||||
if self._client and self.is_connected:
|
||||
LOGGER.debug("Disconnecting device %s", self.name)
|
||||
try:
|
||||
self._data_event.clear()
|
||||
await self._client.disconnect()
|
||||
except BleakError:
|
||||
LOGGER.warning("Disconnect failed!")
|
||||
|
||||
self._client = None
|
||||
127
custom_components/hunterdouglas_powerview_ble/config_flow.py
Normal file
127
custom_components/hunterdouglas_powerview_ble/config_flow.py
Normal file
@@ -0,0 +1,127 @@
|
||||
"""Config flow for BLE Battery Management System integration."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.bluetooth import (
|
||||
BluetoothServiceInfoBleak,
|
||||
async_discovered_service_info,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigFlowResult
|
||||
from homeassistant.const import CONF_ADDRESS
|
||||
|
||||
# from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
|
||||
|
||||
from .const import DOMAIN, LOGGER, MFCT_ID, UUID
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for BT Battery Management System."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 0
|
||||
|
||||
@dataclass
|
||||
class DiscoveredDevice:
|
||||
"""A discovered bluetooth device."""
|
||||
|
||||
name: str
|
||||
discovery_info: BluetoothServiceInfoBleak
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the config flow."""
|
||||
|
||||
self._discovered_device: ConfigFlow.DiscoveredDevice | None = None
|
||||
self._discovered_devices: dict[str, ConfigFlow.DiscoveredDevice] = {}
|
||||
|
||||
async def async_step_bluetooth(
|
||||
self, discovery_info: BluetoothServiceInfoBleak
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initialized by Bluetooth discovery."""
|
||||
LOGGER.debug("Bluetooth device detected: %s", discovery_info)
|
||||
|
||||
await self.async_set_unique_id(discovery_info.address)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
self._discovered_device = ConfigFlow.DiscoveredDevice(
|
||||
discovery_info.name, discovery_info
|
||||
)
|
||||
self.context["title_placeholders"] = {"name": self._discovered_device.name}
|
||||
return await self.async_step_bluetooth_confirm()
|
||||
|
||||
async def async_step_bluetooth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm bluetooth device discovery."""
|
||||
assert self._discovered_device is not None
|
||||
LOGGER.debug("confirm step for %s", self._discovered_device.name)
|
||||
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(
|
||||
title=self._discovered_device.name,
|
||||
data={},
|
||||
)
|
||||
|
||||
self._set_confirm_only()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="bluetooth_confirm",
|
||||
description_placeholders={"name": self._discovered_device.name},
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the user step to pick discovered device."""
|
||||
LOGGER.debug("user step")
|
||||
|
||||
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 = self._discovered_devices[address]
|
||||
|
||||
self.context["title_placeholders"] = {"name": self._discovered_device.name}
|
||||
|
||||
return self.async_create_entry(
|
||||
title=self._discovered_device.name,
|
||||
data={},
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
if MFCT_ID not in discovery_info.manufacturer_data:
|
||||
continue
|
||||
|
||||
if UUID not in discovery_info.service_uuids:
|
||||
continue
|
||||
|
||||
self._discovered_devices[address] = ConfigFlow.DiscoveredDevice(
|
||||
discovery_info.name, discovery_info
|
||||
)
|
||||
|
||||
if not self._discovered_devices:
|
||||
return self.async_abort(reason="no_devices_found")
|
||||
|
||||
titles = []
|
||||
for address, discovery in self._discovered_devices.items():
|
||||
titles.append({"value": address, "label": discovery.name})
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ADDRESS): SelectSelector(
|
||||
SelectSelectorConfig(options=titles)
|
||||
)
|
||||
}
|
||||
),
|
||||
)
|
||||
21
custom_components/hunterdouglas_powerview_ble/const.py
Normal file
21
custom_components/hunterdouglas_powerview_ble/const.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""Constants for the BLE Battery Management System integration."""
|
||||
|
||||
import logging
|
||||
|
||||
# from homeassistant.const import ( # noqa: F401
|
||||
# ATTR_BATTERY_CHARGING,
|
||||
# ATTR_BATTERY_LEVEL,
|
||||
# ATTR_TEMPERATURE,
|
||||
# ATTR_VOLTAGE,
|
||||
# )
|
||||
|
||||
|
||||
DOMAIN = "hunterdouglas_powerview_ble"
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
UPDATE_INTERVAL = 30 # in seconds
|
||||
UUID = "0000fdc1-0000-1000-8000-00805f9b34fb"
|
||||
MFCT_ID = 2073
|
||||
|
||||
# attributes (do not change)
|
||||
ATTR_RSSI = "rssi"
|
||||
|
||||
90
custom_components/hunterdouglas_powerview_ble/coordinator.py
Normal file
90
custom_components/hunterdouglas_powerview_ble/coordinator.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""Home Assistant coordinator for Hunter Douglas PowerView (BLE) integration."""
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from bleak.backends.device import BLEDevice
|
||||
from bleak.exc import BleakError
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.components.bluetooth import DOMAIN as BLUETOOTH_DOMAIN
|
||||
from homeassistant.const import ATTR_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .api import PowerViewBLE
|
||||
from .const import ATTR_RSSI, DOMAIN, LOGGER, UPDATE_INTERVAL
|
||||
|
||||
|
||||
class PVCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
"""Update coordinator for a battery management system."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
ble_device: BLEDevice,
|
||||
) -> None:
|
||||
"""Initialize BMS data coordinator."""
|
||||
assert ble_device.name is not None
|
||||
super().__init__(
|
||||
hass=hass,
|
||||
logger=LOGGER,
|
||||
name=ble_device.name,
|
||||
update_interval=timedelta(seconds=UPDATE_INTERVAL),
|
||||
always_update=False, # only update when sensor value has changed
|
||||
)
|
||||
|
||||
self._mac = ble_device.address
|
||||
self.api = PowerViewBLE(ble_device)
|
||||
LOGGER.debug(
|
||||
"Initializing coordinator for %s (%s)",
|
||||
ble_device.name,
|
||||
ble_device.address,
|
||||
)
|
||||
if service_info := bluetooth.async_last_service_info(
|
||||
self.hass, address=self._mac, connectable=True
|
||||
):
|
||||
LOGGER.debug("device data: %s", service_info.as_dict())
|
||||
|
||||
self.device_info = DeviceInfo(
|
||||
identifiers={
|
||||
(DOMAIN, ble_device.name),
|
||||
(BLUETOOTH_DOMAIN, ble_device.address),
|
||||
},
|
||||
connections={(CONNECTION_BLUETOOTH, ble_device.address)},
|
||||
name=ble_device.name,
|
||||
configuration_url=None,
|
||||
# properties used in GUI:
|
||||
manufacturer="Hunter Douglas",
|
||||
model="shade",
|
||||
)
|
||||
|
||||
@property
|
||||
def address(self) -> str:
|
||||
"""Return MAC address of remote device."""
|
||||
return self._mac
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Return the latest data from the device."""
|
||||
LOGGER.debug("%s data update", self.device_info.get(ATTR_NAME))
|
||||
|
||||
try:
|
||||
info = {} # await self._device.async_update()
|
||||
except TimeoutError:
|
||||
LOGGER.debug("Device communication timeout")
|
||||
raise
|
||||
except BleakError as err:
|
||||
raise UpdateFailed(
|
||||
f"device communicating failed: {err!s} ({type(err).__name__})"
|
||||
) from err
|
||||
|
||||
if (
|
||||
service_info := bluetooth.async_last_service_info(
|
||||
self.hass, address=self._mac, connectable=True
|
||||
)
|
||||
) is not None:
|
||||
info.update({ATTR_RSSI: service_info.rssi})
|
||||
|
||||
LOGGER.debug("data sample %s", info)
|
||||
return info
|
||||
94
custom_components/hunterdouglas_powerview_ble/cover.py
Normal file
94
custom_components/hunterdouglas_powerview_ble/cover.py
Normal file
@@ -0,0 +1,94 @@
|
||||
"""Hunter Douglas Powerview cover."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_CURRENT_POSITION,
|
||||
ATTR_POSITION,
|
||||
CoverDeviceClass,
|
||||
CoverEntity,
|
||||
CoverEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_IDENTIFIERS, ATTR_NAME
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo, format_mac
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
from .coordinator import PVCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the demo cover platform."""
|
||||
|
||||
coordinator: PVCoordinator = config_entry.runtime_data
|
||||
async_add_entities([PowerViewCover(coordinator)])
|
||||
|
||||
|
||||
class PowerViewCover(CoverEntity):
|
||||
"""Representation of a powerview shade."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_device_class = CoverDeviceClass.SHADE
|
||||
_attr_supported_features = (
|
||||
CoverEntityFeature.OPEN
|
||||
| CoverEntityFeature.CLOSE
|
||||
| CoverEntityFeature.SET_POSITION
|
||||
# | CoverEntityFeature.STOP
|
||||
)
|
||||
_attr_current_cover_position: int | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PVCoordinator,
|
||||
) -> None:
|
||||
"""Initialize the shade."""
|
||||
self._attr_name = CoverDeviceClass.SHADE
|
||||
self._coord = coordinator
|
||||
self._attr_device_info = self._coord.device_info
|
||||
self._attr_unique_id = (
|
||||
f"{DOMAIN}_{format_mac(self._coord.address)}_{CoverDeviceClass.SHADE}"
|
||||
)
|
||||
self._attr_current_cover_position: int | None = 0
|
||||
|
||||
@property
|
||||
def is_closed(self) -> bool: # type: ignore[reportIncompatibleVariableOverride]
|
||||
"""Return if the cover is closed."""
|
||||
return self._attr_current_cover_position == 0
|
||||
|
||||
async def async_set_cover_position(self, **kwargs: Any) -> None:
|
||||
"""Move the cover to a specific position."""
|
||||
if pos := kwargs[ATTR_POSITION]:
|
||||
LOGGER.debug("set cover to position %i", pos)
|
||||
try:
|
||||
await self._coord.api.set_position(pos)
|
||||
self._attr_current_cover_position = pos
|
||||
except TimeoutError:
|
||||
pass
|
||||
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""Open the cover."""
|
||||
LOGGER.debug("open cover")
|
||||
try:
|
||||
await self._coord.api.activate_scene(2)
|
||||
self._attr_current_cover_position = 100
|
||||
except TimeoutError:
|
||||
pass
|
||||
|
||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||
"""Close the cover tilt."""
|
||||
LOGGER.debug("close cover")
|
||||
try:
|
||||
await self._coord.api.activate_scene(3)
|
||||
self._attr_current_cover_position = 0
|
||||
except TimeoutError:
|
||||
pass
|
||||
|
||||
async def async_stop_cover(self, **kwargs: Any) -> None:
|
||||
"""Stop the cover."""
|
||||
LOGGER.debug("stop cover")
|
||||
21
custom_components/hunterdouglas_powerview_ble/manifest.json
Normal file
21
custom_components/hunterdouglas_powerview_ble/manifest.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"domain": "hunterdouglas_powerview_ble",
|
||||
"name": "Hunter Douglas PowerView (BLE)",
|
||||
"bluetooth": [
|
||||
{
|
||||
"service_uuid": "0000fdc1-0000-1000-8000-00805f9b34fb",
|
||||
"manufacturer_id": 2073,
|
||||
"manufacturer_data_start": [0,0,42,0]
|
||||
}
|
||||
],
|
||||
"codeowners": ["@patman15"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://github.com/patman15/hdpv_ble",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"issue_tracker": "https://github.com/patman15/hdpv_ble/issues",
|
||||
"loggers": ["hunterdouglas_powerview_ble"],
|
||||
"requirements": [],
|
||||
"version": "0.1"
|
||||
}
|
||||
15
custom_components/hunterdouglas_powerview_ble/strings.json
Normal file
15
custom_components/hunterdouglas_powerview_ble/strings.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "Setup {name}",
|
||||
"step": {
|
||||
"bluetooth_confirm": {
|
||||
"description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
|
||||
"not_supported": "Device not supported"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured",
|
||||
"no_devices_found": "No devices found on the network",
|
||||
"not_supported": "Device not supported"
|
||||
},
|
||||
"flow_title": "Setup {name}",
|
||||
"step": {
|
||||
"bluetooth_confirm": {
|
||||
"description": "Do you want to set up {name}?"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
5
hacs.json
Normal file
5
hacs.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Hunter Douglas PowerView (BLE)",
|
||||
"homeassistant": "2024.6.0",
|
||||
"render_readme": true
|
||||
}
|
||||
16
pyproject.toml
Normal file
16
pyproject.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
# pyproject.toml
|
||||
|
||||
#[tool.setuptools.packages.find]
|
||||
#where = ["custom_components/"]
|
||||
#include = ["bms_ble"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
minversion = "8.0"
|
||||
addopts="--cov=custom_components.bms_ble --cov-report=term-missing --cov-fail-under=100"
|
||||
pythonpath = [
|
||||
"custom_components.hunterdouglas_powerview_ble",
|
||||
]
|
||||
testpaths = [
|
||||
"tests",
|
||||
]
|
||||
asyncio_mode = "auto"
|
||||
4
requirements.txt
Normal file
4
requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
homeassistant==2024.6.0
|
||||
pip>=21.3.1
|
||||
ruff==0.4.2
|
||||
|
||||
25
requirements_test.txt
Normal file
25
requirements_test.txt
Normal file
@@ -0,0 +1,25 @@
|
||||
pip>=21.3.1
|
||||
homeassistant==2024.6.0
|
||||
wheel
|
||||
home-assistant-bluetooth
|
||||
habluetooth>=2.4.0
|
||||
bluetooth-adapters
|
||||
pytest>=8.0.2
|
||||
pytest-cov>=4.0.0
|
||||
pytest-socket>=0.5.0
|
||||
pytest-asyncio
|
||||
sqlalchemy
|
||||
freezegun
|
||||
requests-mock
|
||||
syrupy>=4.1.0
|
||||
aiohttp
|
||||
aiohttp_cors
|
||||
aiohttp-fast-url-dispatcher
|
||||
aiohttp-zlib-ng
|
||||
bleak>=0.19.0
|
||||
bleak-retry-connector>=3.3.0
|
||||
bluetooth-data-tools
|
||||
pyserial-asyncio
|
||||
pyudev
|
||||
pytest-homeassistant-custom-component==0.13.132
|
||||
|
||||
Reference in New Issue
Block a user