diff --git a/.cookiecutter.json b/.cookiecutter.json index 5df0e48..9b1dfa4 100644 --- a/.cookiecutter.json +++ b/.cookiecutter.json @@ -2,7 +2,7 @@ "_template": "git+ssh://git@github.com/oncleben31/cookiecutter-homeassistant-custom-component", "class_name_prefix": "Bermuda", "domain_name": "bermuda", - "friendly_name": "Bermuda BLE Triangulation", + "friendly_name": "Bermuda BLE Trilateration", "github_user": "agittins", "project_name": "bermuda", "test_suite": "yes", diff --git a/.devcontainer.json b/.devcontainer.json index 025ef17..29a117c 100644 --- a/.devcontainer.json +++ b/.devcontainer.json @@ -3,18 +3,6 @@ "image": "mcr.microsoft.com/vscode/devcontainers/python:0-3.11", "postCreateCommand": "scripts/setup", "forwardPorts": [8123, 5678], - "mounts": [ - { - "source": "/home/agittins/.ssh/id_ed25519", - "target": "/home/vscode/.ssh/id_ed25519", - "type": "bind" - }, - { - "source": "/home/agittins/.ssh/id_ed25519.pub", - "target": "/home/vscode/.ssh/id_ed25519.pub", - "type": "bind" - } - ], "portsAttributes": { "8123": { "label": "Home Assistant", @@ -31,6 +19,7 @@ "ms-python.python", "github.vscode-pull-request-github", "ryanluker.vscode-coverage-gutters", + "ms-python.pylint", "ms-python.vscode-pylance", "github.vscode-github-actions" ], diff --git a/.vscode/launch.json b/.vscode/launch.json index cc5337a..f5c3d6c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,6 +9,7 @@ "request": "attach", "port": 5678, "host": "localhost", + "justMyCode": false, "pathMappings": [ { "localRoot": "${workspaceFolder}", diff --git a/README.md b/README.md index cc35ad3..63b938b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ -# Bermuda BLE Triangulation +![Bermuda Logo](img/logo@2x.png) -Triangulate your lost objects using ESPHome bluetooth proxies! +# Bermuda BLE Trilateration + +(eventually) Triangulate your lost objects using ESPHome bluetooth proxies! [![GitHub Release][releases-shield]][releases] [![GitHub Activity][commits-shield]][commits] @@ -16,7 +18,7 @@ Triangulate your lost objects using ESPHome bluetooth proxies! [![Discord][discord-shield]][discord] [![Community Forum][forum-shield]][forum] -##STATUS: Early days! +##STATUS: Early days! No triangulation yet, but area-based location tracking works - Can replace bluetooth_ble_tracker by creating entities for home/not_home for selected BLE devices, which can be used for Person home/away sensing. @@ -32,7 +34,7 @@ Triangulate your lost objects using ESPHome bluetooth proxies! closest BLE Proxy. If you have a bluetooth receiver (ESPHome with `bluetooth_proxy` or a Shelley device) in each room you want tracking for, this will do the job. -- (soon) Provide a mud-map of your entire home, in bluetooth signal strength terms. +- (soon?) Provide a mud-map of your entire home, in bluetooth signal strength terms. This integration uses the advertisement data gathered by your esphome or Shelley bluetooth-proxy deployments to track or triangulate (more correctly, @@ -41,45 +43,54 @@ observed around your home. Note that this is more properly called "Tri*lateration*", as we are not measuring the angles, but instead measuring distances. The bottom line -is that triangulation is more likely to hit people's search terms. +is that triangulation is more likely to hit people's search terms so we'll +probably bandy that term about a bit :-) This integration gives you two forms of presence tracking. -- Simple Home/Away detection using the device_tracker integration. This is +- Simple Home/Away detection using the [device_tracker](https://www.home-assistant.io/integrations/device_tracker/) integration. This is not much different to the already working bluetooth_le_tracker integration - in that regard, but was an easy step along the way to... + in that regard, but doesn't use the `known_devices.yaml` file, lets you create + entities only for the devices you want to track, and was an easy step along the way to... - Room-based ("Area"s in homeassistant parlance) localisation for bluetooth devices. For example, "which human/pet is at home and in what room are they?" and "where's my phone/toothbrush?" ## FAQ -Isn't mmWave better? +### Why do my bluetooth devices have only the address and no name? + +- you need to tell your bluetooth proxies to send an inquiry in response to + advertisements. In esphome, this is done by adding `active: true` to the + `bluetooth_proxy` section (this is separate from the active property of + the `esp32_ble_tracker` section, which controls outbound client connections). -: mmWave is definitely _faster_, but it will only tell you "someone" has entered -a space, while Bermuda can tell you _who_ is in a space. +### Isn't mmWave better? -What about PIR / Infrared? +- mmWave is definitely _faster_, but it will only tell you "someone" has entered + a space, while Bermuda can tell you _who_ is in a space. -: It's also likely faster than bluetooth, but again it only tells you that -someone / something is present, but doesn't tell you who/what. +### What about PIR / Infrared? + +- It's also likely faster than bluetooth, but again it only tells you that + someone / something is present, but doesn't tell you who/what. So how does that help? -: If the home knows who is in a given room, it can set the thermostat to their -personal preferences, or perhaps their lighting settings. This might be -particularly useful for testing automations for yourself before unleashing them -on to your housemates, so they don't get annoyed while you iron out the bugs :-) +- If the home knows who is in a given room, it can set the thermostat to their + personal preferences, or perhaps their lighting settings. This might be + particularly useful for testing automations for yourself before unleashing them + on to your housemates, so they don't get annoyed while you iron out the bugs :-) -: If you have BLE tags on your pets you can have automations specifically for them, -and/or you can exclude certain automations, for example don't trigger a light from -an IR sensor if it knows it's just your cat, say. +- If you have BLE tags on your pets you can have automations specifically for them, + and/or you can exclude certain automations, for example don't trigger a light from + an IR sensor if it knows it's just your cat, say. -How quickly does it react? +### How quickly does it react? -: That will mainly depend on how often your beacon transmits advertisements, however -right now the integration only re-calculates on a timed basis. This should be changed -to a realtime recalculation based on incoming advertisements soon. +- That will mainly depend on how often your beacon transmits advertisements, however + right now the integration only re-calculates on a timed basis. This should be changed + to a realtime recalculation based on incoming advertisements soon. ## What you need @@ -119,7 +130,7 @@ devices in your home that are sending broadcasts. The implemented results are: [x] A raw listing of values returned when you call the `bermuda.dump_devices` service [x] `area` if a device is within a max distance of a receiver -[] An interface to choose which devices should have sensors created for them +[x] An interface to choose which devices should have sensors created for them [x] Sensors created for selected devices, showing their estimated location [] Algo to "solve" the 2D layout of devices [] A mud-map showing relative locations between proxies and detected devices @@ -135,7 +146,7 @@ devices in your home that are sending broadcasts. The implemented results are: [] Some sort of map, just pick two proxies as an x-axis vector and go [] Config setting to define absolute locations of two proxies [] Support some way to "pin" more than two proxies/tags, and have it not break. -[] Create entities (use `device_tracker`? or create own?) for each detected beacon +[x] Create entities (use `device_tracker`? or create own?) for each detected beacon [] Experiment with some of [these algo's](https://mdpi-res.com/d_attachment/applsci/applsci-10-02003/article_deploy/applsci-10-02003.pdf?version=1584265508) for improving accuracy (too much math for me!). Particularly weighting shorter @@ -173,11 +184,10 @@ a fair amount of ESPrescense's wheel. **This component will set up the following platforms.** -| Platform | Description | -| --------------- | -------------- | -| `binary_sensor` | Nothing yet. | -| `sensor` | Nor here, yet. | -| `switch` | Nope. | +| Platform | Description | +| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `sensor` | For any bluetooth devices you select in the integration's configuration, a device will be created with two sensors, tracking the "Area" and "Distance" from that area. | +| `device_tracker` | A device tracker entity will be created for each configured bluetooth address, which you can use to influence the "Home/Away" state of a "Person". | ## Installation @@ -185,9 +195,11 @@ Definitely use the HACS interface! Once you have HACS installed, go to `Integrat meatballs menu in the top right, and choose `Custom Repositories`. Paste `agittins/bermuda` into the `Repository` field, and choose `Integration` for the `Category`. Click `Add`. -You should now be able to add the `Bermuda BLE Triangulation` integration. Once you have done that, +You should now be able to add the `Bermuda BLE Trilateration` integration. Once you have done that, you need to restart Homeassistant, then in `Settings`, `Devices & Services` choose `Add Integration` -and search for `Bermuda BLE Triangulation`. +and search for `Bermuda BLE Trilateration`. + +In the `Configuration` dialog, you can choose which bluetooth devices you would like the integration to track. The instructions below are the generic notes from the template: @@ -197,7 +209,7 @@ The instructions below are the generic notes from the template: 4. Download _all_ the files from the `custom_components/bermuda/` directory (folder) in this repository. 5. Place the files you downloaded in the new directory (folder) you created. 6. Restart Home Assistant -7. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Bermuda BLE Triangulation" +7. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Bermuda BLE Trilateration" diff --git a/custom_components/bermuda/__init__.py b/custom_components/bermuda/__init__.py index 2177461..a21b4db 100644 --- a/custom_components/bermuda/__init__.py +++ b/custom_components/bermuda/__init__.py @@ -1,5 +1,5 @@ """ -Custom integration to integrate Bermuda BLE Triangulation with Home Assistant. +Custom integration to integrate Bermuda BLE Trilateration with Home Assistant. For more details about this integration, please refer to https://github.com/agittins/bermuda @@ -11,16 +11,24 @@ from datetime import datetime from datetime import timedelta from typing import Final +from typing import TYPE_CHECKING from homeassistant.components import bluetooth +from homeassistant.components.bluetooth import BluetoothChange from homeassistant.components.bluetooth import BluetoothScannerDevice +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak +from homeassistant.components.bluetooth.active_update_coordinator import ( + PassiveBluetoothDataUpdateCoordinator, +) from homeassistant.config_entries import ConfigEntry +from homeassistant.core import callback from homeassistant.core import Config from homeassistant.core import HomeAssistant from homeassistant.core import SupportsResponse from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import area_registry from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util import slugify @@ -28,17 +36,26 @@ from homeassistant.util.dt import monotonic_time_coarse from homeassistant.util.dt import now +from .const import CONF_ATTENUATION +from .const import CONF_DEVICES +from .const import CONF_DEVTRACK_TIMEOUT +from .const import CONF_MAX_RADIUS +from .const import CONF_REF_POWER +from .const import DEFAULT_ATTENUATION +from .const import DEFAULT_DEVTRACK_TIMEOUT +from .const import DEFAULT_MAX_RADIUS +from .const import DEFAULT_REF_POWER from .const import DOMAIN from .const import PLATFORMS from .const import STARTUP_MESSAGE from .entity import BermudaEntity -# from typing import Any +# from bthome_ble import BTHomeBluetoothDeviceData -# from .const import CONF_PASSWORD -# from .const import CONF_USERNAME +if TYPE_CHECKING: + from bleak.backends.device import BLEDevice -SCAN_INTERVAL = timedelta(seconds=10) +SCAN_INTERVAL = timedelta(seconds=5) MONOTONIC_TIME: Final = monotonic_time_coarse @@ -63,7 +80,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # username = entry.data.get(CONF_USERNAME) # password = entry.data.get(CONF_PASSWORD) - coordinator = BermudaDataUpdateCoordinator(hass) + coordinator = BermudaDataUpdateCoordinator(hass, entry) await coordinator.async_refresh() if not coordinator.last_update_success: @@ -82,7 +99,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): return True -def rssi_to_metres(rssi): +def rssi_to_metres(rssi, ref_power=None, attenuation=None): """Convert instant rssi value to a distance in metres Based on the information from @@ -99,13 +116,56 @@ def rssi_to_metres(rssi): tune the attenuation in real time based on changing values coming from known-fixed beacons (eg thermometers, window sensors etc) """ - attenuation = 3.0 # Will range depending on environmental factors - ref_power = -55.0 # db reference measured at 1.0m + if ref_power is None: + return False + # ref_power = self.ref_power + if attenuation is None: + return False + # attenuation= self.attenuation distance = 10 ** ((ref_power - rssi) / (10 * attenuation)) return distance +class BermudaPBDUCoordinator( + PassiveBluetoothDataUpdateCoordinator, +): + """Class for receiving bluetooth adverts in realtime + + Looks like this needs to be run through setup, with a specific + BLEDevice (address) already specified, so won't do "all adverts" + """ + + def __init__( + self, + hass: HomeAssistant, + logger: logging.Logger, + ble_device: BLEDevice, + device: BermudaDevice, + ) -> None: + """Init""" + super().__init__( + hass=hass, + logger=logger, + address=ble_device.address, + mode=bluetooth.BluetoothScanningMode.ACTIVE, + connectable=False, + ) + self.device = device + + @callback + def _async_handle_unavailable( + self, service_info: BluetoothServiceInfoBleak + ) -> None: + return super()._async_handle_unavailable(service_info) + + @callback + def _async_handle_bluetooth_event( + self, service_info: BluetoothServiceInfoBleak, change: BluetoothChange + ) -> None: + return super()._async_handle_bluetooth_event(service_info, change) + + class BermudaDeviceScanner(dict): """Represents details from a scanner relevant to a specific device @@ -117,14 +177,24 @@ class BermudaDeviceScanner(dict): """ def __init__( - self, device_address: str, scandata: BluetoothScannerDevice, area_id: str + self, + device_address: str, + scandata: BluetoothScannerDevice, + area_id: str, + options, ): self.name: str = scandata.scanner.name self.area_id: str = area_id self.adapter: str = scandata.scanner.adapter self.source: str = scandata.scanner.source self.rssi: float = scandata.advertisement.rssi - self.rssi_distance: float = rssi_to_metres(self.rssi) + self.tx_power: float = scandata.advertisement.tx_power + self.options = options + self.rssi_distance: float = rssi_to_metres( + self.rssi, + options.get(CONF_REF_POWER, DEFAULT_REF_POWER), + options.get(CONF_ATTENUATION, DEFAULT_ATTENUATION), + ) self.adverts: dict[str, bytes] = scandata.advertisement.service_data.items() self.stamp: float = None @@ -185,9 +255,10 @@ class BermudaDevice(dict): become entities in homeassistant, since there might be a _lot_ of them. """ - def __init__(self): + def __init__(self, address, options): """Initial (empty) data""" - self.address: str = None + self.address: str = address + self.options = options self.unique_id: str = None # mac address formatted. self.name: str = None self.local_name: str = None @@ -217,6 +288,7 @@ def add_scanner( self.address, discoveryinfo, # the entire BluetoothScannerDevice struct scanner_device.area_id, + self.options, ) # Let's see if we should update our last_seen based on this... if self.last_seen < newscanner.stamp: @@ -272,17 +344,15 @@ class BermudaDataUpdateCoordinator(DataUpdateCoordinator): def __init__( self, hass: HomeAssistant, + entry: ConfigEntry, ) -> None: """Initialize.""" + # self.config_entry = entry self.platforms = [] self.devices: dict[str, BermudaDevice] = {} self.created_entities: set[BermudaEntity] = set() - self.ar = area_registry.async_get(hass) - - # TODO: These settings are to be moved into the config flow - self.max_area_radius = 3.0 # maximum distance to consider "in the area" - self.timeout_not_home = 60 # seconds to wait before declaring "not_home" + self.area_reg = area_registry.async_get(hass) hass.services.async_register( DOMAIN, @@ -292,6 +362,22 @@ def __init__( SupportsResponse.ONLY, ) + self.options = {} + if hasattr(entry, "options"): + # Firstly, on some calls (specifically during reload after settings changes) + # we seem to get called with a non-existant config_entry. + # Anyway... if we DO have one, convert it to a plain dict so we can + # serialise it properly when it goes into the device and scanner classes. + for key, val in entry.options.items(): + if key in ( + CONF_ATTENUATION, + CONF_DEVICES, + CONF_DEVTRACK_TIMEOUT, + CONF_MAX_RADIUS, + CONF_REF_POWER, + ): + self.options[key] = val + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) def _get_device(self, address: str) -> BermudaDevice: @@ -305,7 +391,9 @@ def _get_or_create_device(self, address: str) -> BermudaDevice: device = self._get_device(address) if device is None: mac = format_mac(address) - self.devices[mac] = device = BermudaDevice() + self.devices[mac] = device = BermudaDevice( + address=address, options=self.options + ) device.address = mac device.unique_id = mac return device @@ -329,21 +417,22 @@ async def _async_update_data(self): # We probably don't need to do all of this every time, but we # want to catch any changes, eg when the system learns the local # name etc. - device.name = service_info.device.name - device.local_name = service_info.advertisement.local_name - device.manufacturer = service_info.manufacturer + device.name = device.name or service_info.device.name + device.local_name = ( + device.local_name or service_info.advertisement.local_name + ) + device.manufacturer = device.manufacturer or service_info.manufacturer device.connectable = service_info.connectable # Try to make a nice name for prefname. # TODO: Add support for user-defined name, especially since the # device_tracker entry can only be renamed using the editor. - if service_info.advertisement.local_name is not None: - device.prefname = service_info.advertisement.local_name - elif service_info.device.name is not None: - device.prefname = service_info.device.name - else: - # we tried. Fall back to boring... - device.prefname = "bermuda_" + slugify(service_info.address) + if device.prefname is None or device.prefname.startswith(DOMAIN + "_"): + device.prefname = ( + device.name + or device.local_name + or DOMAIN + "_" + slugify(device.address) + ) # Work through the scanner entries... matched_scanners = bluetooth.async_scanner_devices_by_address( @@ -371,12 +460,7 @@ async def _async_update_data(self): # Replace the scanner entry on the current device device.add_scanner(scanner_device, discovered) - # FIXME: This should be configurable... - if device.address.upper() in [ - "EE:E8:37:9F:6B:54", # infinitime, main watch - "C7:B8:C6:B0:27:11", # pinetime, devwatch - "A4:C1:38:C8:58:91", # bthome thermo, with reed switch - ]: + if device.address.upper() in self.options.get(CONF_DEVICES, []): device.send_tracker_see = True device.create_sensor = True @@ -404,7 +488,11 @@ async def _send_device_tracker_see(self, device: BermudaDevice): """ # Check if the device has been seen recently - if MONOTONIC_TIME() - self.timeout_not_home < device.last_seen: + if ( + MONOTONIC_TIME() + - self.options.get(CONF_DEVTRACK_TIMEOUT, DEFAULT_DEVTRACK_TIMEOUT) + < device.last_seen + ): device.zone = "home" else: device.zone = "not_home" @@ -449,7 +537,9 @@ def _refresh_area_by_min_distance(self, device: BermudaDevice): for scanner in device.scanners.values(): # whittle down to the closest beacon inside max range - if scanner.rssi_distance < self.max_area_radius: # potential... + if scanner.rssi_distance < self.options.get( + CONF_MAX_RADIUS, DEFAULT_MAX_RADIUS + ): # potential... if ( closest_scanner is None or scanner.rssi_distance < closest_scanner.rssi_distance @@ -458,7 +548,9 @@ def _refresh_area_by_min_distance(self, device: BermudaDevice): if closest_scanner is not None: # We found a winner device.area_id = closest_scanner.area_id - areas = self.ar.async_get_area(device.area_id).name # potentially a list?! + areas = self.area_reg.async_get_area( + device.area_id + ).name # potentially a list?! if len(areas) == 1: device.area_name = areas[0] else: @@ -502,6 +594,30 @@ async def service_dump_devices(self, call): # pylint: disable=unused-argument; return out +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry +) -> bool: + """Remove a config entry from a device.""" + coordinator: BermudaDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + address = None + for ident in device_entry.identifiers: + try: + if ident[0] == DOMAIN: + # the identifier should be the mac address, and + # may have "_range" or some other per-sensor suffix. Just grab + # the mac address part. + address = ident[1][:17] + except KeyError: + pass + if address is not None: + try: + coordinator.devices[format_mac(address)].create_sensor = False + except KeyError: + _LOGGER.warning("Failed to locate device entry for %s", address) + return True + return False + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Handle removal of an entry.""" coordinator = hass.data[DOMAIN][entry.entry_id] diff --git a/custom_components/bermuda/binary_sensor.py b/custom_components/bermuda/binary_sensor.py index 91ccff4..2e1c106 100644 --- a/custom_components/bermuda/binary_sensor.py +++ b/custom_components/bermuda/binary_sensor.py @@ -1,4 +1,4 @@ -"""Binary sensor platform for Bermuda BLE Triangulation.""" +"""Binary sensor platform for Bermuda BLE Trilateration.""" from homeassistant.components.binary_sensor import BinarySensorEntity from .const import BINARY_SENSOR diff --git a/custom_components/bermuda/config_flow.py b/custom_components/bermuda/config_flow.py index c791086..29e90a4 100644 --- a/custom_components/bermuda/config_flow.py +++ b/custom_components/bermuda/config_flow.py @@ -1,12 +1,23 @@ -"""Adds config flow for Bermuda BLE Triangulation.""" +"""Adds config flow for Bermuda BLE Trilateration.""" import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import bluetooth +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak from homeassistant.core import callback - -from .const import CONF_PASSWORD -from .const import CONF_USERNAME +from homeassistant.helpers.config_entry_flow import FlowResult +from homeassistant.helpers.selector import selector + +from .const import CONF_ATTENUATION +from .const import CONF_DEVICES +from .const import CONF_DEVTRACK_TIMEOUT +from .const import CONF_MAX_RADIUS +from .const import CONF_REF_POWER +from .const import DEFAULT_ATTENUATION +from .const import DEFAULT_REF_POWER from .const import DOMAIN -from .const import PLATFORMS +from .const import NAME + +# from homeassistant import data_entry_flow # from homeassistant.helpers.aiohttp_client import async_create_clientsession @@ -21,55 +32,55 @@ def __init__(self): """Initialize.""" self._errors = {} + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> FlowResult: + """Support automatic initiation of setup through bluetooth discovery. + (we still show a confirmation form to the user, though) + This is triggered by discovery matchers set in manifest.json, + and since we track any BLE advert, we're being a little cheeky by listing any. + """ + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + # Create a unique ID so that we don't get multiple discoveries appearing. + await self.async_set_unique_id(DOMAIN) + self._abort_if_unique_id_configured() + + return self.async_show_form(step_id="user", description_placeholders=NAME) + async def async_step_user(self, user_input=None): - """Handle a flow initialized by the user.""" - self._errors = {} + """Handle a flow initialized by the user. - # Uncomment the next 2 lines if only a single instance of - # the integration is allowed: - # if self._async_current_entries(): - # return self.async_abort(reason="single_instance_allowed") + We don't need any config for base setup, so we just activate + (but only for one instance) + """ + + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") if user_input is not None: - valid = await self._test_credentials( - user_input[CONF_USERNAME], user_input[CONF_PASSWORD] + # create the integration! + return self.async_create_entry( + title=NAME, data={"source": "user"}, description=NAME ) - if valid: - return self.async_create_entry( - title=user_input[CONF_USERNAME], data=user_input - ) - else: - self._errors["base"] = "auth" - - return await self._show_config_form(user_input) - return await self._show_config_form(user_input) + return self.async_show_form(step_id="user", description_placeholders=NAME) @staticmethod @callback def async_get_options_flow(config_entry): return BermudaOptionsFlowHandler(config_entry) - async def _show_config_form(self, user_input): # pylint: disable=unused-argument - """Show the configuration form to edit location data.""" - return self.async_show_form( - step_id="user", - data_schema=vol.Schema( - {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} - ), - errors=self._errors, - ) - - async def _test_credentials(self, username, password): - """Return true if credentials is valid.""" - try: - # session = async_create_clientsession(self.hass) - # client = BermudaApiClient(username, password, session) - # await client.async_get_data() - return True - except Exception: # pylint: disable=broad-except - pass - return False + # async def _show_config_form(self, user_input): # pylint: disable=unused-argument + # """Show the configuration form to edit location data.""" + # return self.async_show_form( + # step_id="user", + # data_schema=vol.Schema( + # {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} + # ), + # errors=self._errors, + # ) class BermudaOptionsFlowHandler(config_entries.OptionsFlow): @@ -82,26 +93,65 @@ def __init__(self, config_entry): async def async_step_init(self, user_input=None): # pylint: disable=unused-argument """Manage the options.""" - return await self.async_step_user() + return await self.async_step_globalopts() - async def async_step_user(self, user_input=None): + async def async_step_globalopts(self, user_input=None): """Handle a flow initialized by the user.""" if user_input is not None: self.options.update(user_input) return await self._update_options() - return self.async_show_form( - step_id="user", - data_schema=vol.Schema( + options = [] + for service_info in bluetooth.async_discovered_service_info(self.hass, False): + options.append( + { + "value": service_info.address.upper(), + "label": f"[{service_info.address}] {service_info.name or service_info.advertisement.local_name or service_info.device.name}", + } + ) + + for address in self.options.get(CONF_DEVICES, []): + if not next( + (item for item in options if item["value"] == address.upper()), False + ): + options.append( + {"value": address.upper(), "label": f"[{address}] (saved)"} + ) + + data_schema = { + vol.Required( + CONF_MAX_RADIUS, + default=self.options.get(CONF_MAX_RADIUS, 3.0), + ): vol.Coerce(float), + vol.Required( + CONF_DEVTRACK_TIMEOUT, + default=self.options.get(CONF_DEVTRACK_TIMEOUT, 30), + ): vol.Coerce(int), + vol.Required( + CONF_ATTENUATION, + default=self.options.get(CONF_ATTENUATION, DEFAULT_ATTENUATION), + ): vol.Coerce(float), + vol.Required( + CONF_REF_POWER, + default=self.options.get(CONF_REF_POWER, DEFAULT_REF_POWER), + ): vol.Coerce(float), + vol.Optional( + CONF_DEVICES, + default=self.options.get(CONF_DEVICES, []), + ): selector( { - vol.Required(x, default=self.options.get(x, True)): bool - for x in sorted(PLATFORMS) + "select": { + "options": options, + "multiple": True, + } } ), + } + + return self.async_show_form( + step_id="globalopts", data_schema=vol.Schema(data_schema) ) async def _update_options(self): """Update config entry options.""" - return self.async_create_entry( - title=self.config_entry.data.get(CONF_USERNAME), data=self.options - ) + return self.async_create_entry(title=NAME, data=self.options) diff --git a/custom_components/bermuda/const.py b/custom_components/bermuda/const.py index 2d5888a..9b83bbf 100644 --- a/custom_components/bermuda/const.py +++ b/custom_components/bermuda/const.py @@ -1,6 +1,6 @@ -"""Constants for Bermuda BLE Triangulation.""" +"""Constants for Bermuda BLE Trilateration.""" # Base component constants -NAME = "Bermuda BLE Triangulation" +NAME = "Bermuda BLE Trilateration" DOMAIN = "bermuda" DOMAIN_DATA = f"{DOMAIN}_data" VERSION = "0.0.0" @@ -21,11 +21,25 @@ # PLATFORMS = [BINARY_SENSOR, SENSOR, SWITCH] PLATFORMS = [SENSOR] +DOCS = {} + # Configuration and options -CONF_ENABLED = "enabled" -CONF_USERNAME = "username" -CONF_PASSWORD = "password" -CONF_DEVTRACKERS = "device_trackers" + +CONF_DEVICES = "configured_devices" +DOCS[CONF_DEVICES] = "Identifies which bluetooth devices we wish to expose" + +CONF_MAX_RADIUS, DEFAULT_MAX_RADIUS = "max_area_radius", 20 +DOCS[CONF_MAX_RADIUS] = "For simple area-detection, max radius from receiver" + +CONF_DEVTRACK_TIMEOUT, DEFAULT_DEVTRACK_TIMEOUT = "devtracker_nothome_timeout", 30 +DOCS[ + CONF_DEVTRACK_TIMEOUT +] = "Timeout in seconds for setting devices as `Not Home` / `Away`." + +CONF_ATTENUATION, DEFAULT_ATTENUATION = "attenuation", 3 +DOCS[CONF_ATTENUATION] = "Factor for environmental signal attenuation." +CONF_REF_POWER, DEFAULT_REF_POWER = "ref_power", -55.0 +DOCS[CONF_REF_POWER] = "Default RSSI for signal at 1 metre." # Defaults DEFAULT_NAME = DOMAIN diff --git a/custom_components/bermuda/entity.py b/custom_components/bermuda/entity.py index 861ff40..73d72a5 100644 --- a/custom_components/bermuda/entity.py +++ b/custom_components/bermuda/entity.py @@ -31,7 +31,7 @@ def __init__( self.coordinator = coordinator self.config_entry = config_entry self._device = coordinator.devices[address] - self.ar = area_registry.async_get(coordinator.hass) + self.area_reg = area_registry.async_get(coordinator.hass) @property def unique_id(self): @@ -42,7 +42,7 @@ def unique_id(self): def device_info(self): """Implementing this creates an entry in the device registry.""" return { - "identifiers": {(DOMAIN, self.unique_id)}, + "identifiers": {(DOMAIN, self._device.unique_id)}, "name": self._device.prefname, "model": VERSION, "manufacturer": NAME, diff --git a/custom_components/bermuda/manifest.json b/custom_components/bermuda/manifest.json index d106ad5..a268b4b 100644 --- a/custom_components/bermuda/manifest.json +++ b/custom_components/bermuda/manifest.json @@ -1,6 +1,12 @@ { "domain": "bermuda", - "name": "Bermuda BLE Triangulation", + "name": "Bermuda BLE Trilateration", + "bluetooth": [ + { + "connectable": false, + "manufacturer_id": 76 + } + ], "codeowners": ["@agittins"], "config_flow": true, "dependencies": ["bluetooth_adapters", "device_tracker"], diff --git a/custom_components/bermuda/sensor.py b/custom_components/bermuda/sensor.py index 71d89d4..c2b3afe 100644 --- a/custom_components/bermuda/sensor.py +++ b/custom_components/bermuda/sensor.py @@ -1,8 +1,9 @@ -"""Sensor platform for Bermuda BLE Triangulation.""" +"""Sensor platform for Bermuda BLE Trilateration.""" from collections.abc import Mapping from typing import Any from homeassistant import config_entries +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -23,11 +24,12 @@ async def async_setup_entry( """Setup sensor platform.""" coordinator: BermudaDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - # We simply trawl the devices to set up sensors. + # We go through each "device" in the co-ordinator, and create the entities entities = [] for device in coordinator.devices.values(): if device.create_sensor: entities.append(BermudaSensor(coordinator, entry, device.address)) + entities.append(BermudaSensorRange(coordinator, entry, device.address)) # async_add_devices([BermudaSensor(coordinator, entry)]) async_add_devices(entities, True) @@ -68,3 +70,36 @@ def extra_state_attributes(self) -> Mapping[str, Any] | None: "area_name": self._device.area_name, "area_distance": self._device.area_distance, } + + +class BermudaSensorRange(BermudaSensor): + """Extra sensor for range-to-area + + Note it extends the other sensor, so we only need to set name and value""" + + @property + def unique_id(self): + return super().unique_id + "_range" + + @property + def name(self): + return "Distance" + + @property + def state(self): + distance = self._device.area_distance + if distance is not None: + return round(distance, 3) + return None + + @property + def device_class(self): + return SensorDeviceClass.DISTANCE + + @property + def native_unit_of_measurement(self): + return "m" + + @property + def state_class(self): + return "measurement" diff --git a/custom_components/bermuda/switch.py b/custom_components/bermuda/switch.py index 992a832..04dbefe 100644 --- a/custom_components/bermuda/switch.py +++ b/custom_components/bermuda/switch.py @@ -1,4 +1,4 @@ -"""Switch platform for Bermuda BLE Triangulation.""" +"""Switch platform for Bermuda BLE Trilateration.""" from homeassistant.components.switch import SwitchEntity from .const import DEFAULT_NAME diff --git a/custom_components/bermuda/translations/en.json b/custom_components/bermuda/translations/en.json index e0414b0..bedc70a 100644 --- a/custom_components/bermuda/translations/en.json +++ b/custom_components/bermuda/translations/en.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "title": "Bermuda BLE Triangulation", + "title": "Bermuda BLE Trilateration", "description": "If you need help with the configuration have a look here: https://github.com/agittins/bermuda", "data": { "username": "Username", @@ -19,11 +19,17 @@ }, "options": { "step": { - "user": { + "globalopts": { "data": { - "binary_sensor": "Binary sensor enabled", - "sensor": "Sensor enabled", - "switch": "Switch enabled" + "max_area_radius": "Max radius in metres for simple AREA detection", + "devtracker_nothome_timeout": "Timeout in seconds to consider a device as `Not Home`.", + "attenuation": "Environment attenuation factor for distance calculation.", + "ref_power": "Default rssi at 1 metre distance.", + "configured_devices": "List of Bluetooth devices to specifically create tracking entities for." + }, + "data_description": { + "max_area_radius": "In the simple `AREA` feature, a device will be marked as being in the AREA of it's closest receiver, if inside this radius. If you set it small, devices will go to `unknown` between receivers, but if large devices will always appear as in their closest Area.", + "devtracker_nothome_timeout": "How quickly to mark device_tracker entities as `not_home` after we stop seeing advertisements. 30 seconds or more is probably good." } } } diff --git a/custom_components/bermuda/translations/fr.json b/custom_components/bermuda/translations/fr.json index a666990..7622f5f 100644 --- a/custom_components/bermuda/translations/fr.json +++ b/custom_components/bermuda/translations/fr.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "title": "Bermuda BLE Triangulation", + "title": "Bermuda BLE Trilateration", "description": "Si vous avez besoin d'aide pour la configuration, regardez ici: https://github.com/agittins/bermuda", "data": { "username": "Identifiant", diff --git a/custom_components/bermuda/translations/nb.json b/custom_components/bermuda/translations/nb.json index 3a19853..1c6beb3 100644 --- a/custom_components/bermuda/translations/nb.json +++ b/custom_components/bermuda/translations/nb.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "title": "Bermuda BLE Triangulation", + "title": "Bermuda BLE Trilateration", "description": "Hvis du trenger hjep til konfigurasjon ta en titt her: https://github.com/agittins/bermuda", "data": { "username": "Brukernavn", diff --git a/hacs.json b/hacs.json index 8728932..a6ace8c 100644 --- a/hacs.json +++ b/hacs.json @@ -1,5 +1,5 @@ { - "name": "Bermuda BLE Triangulation", + "name": "Bermuda BLE Trilateration", "hacs": "1.6.0", "homeassistant": "2023.8", "render_readme": true diff --git a/img/bermuda-logos.svg b/img/bermuda-logos.svg new file mode 100644 index 0000000..c22c395 --- /dev/null +++ b/img/bermuda-logos.svg @@ -0,0 +1,425 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Bermuda + BLE + + + + + + + + + diff --git a/img/dark_icon.png b/img/dark_icon.png new file mode 100644 index 0000000..7ea1463 Binary files /dev/null and b/img/dark_icon.png differ diff --git a/img/dark_icon@2x.png b/img/dark_icon@2x.png new file mode 100644 index 0000000..7208e85 Binary files /dev/null and b/img/dark_icon@2x.png differ diff --git a/img/dark_logo.png b/img/dark_logo.png new file mode 100644 index 0000000..a03e710 Binary files /dev/null and b/img/dark_logo.png differ diff --git a/img/dark_logo@2x.png b/img/dark_logo@2x.png new file mode 100644 index 0000000..38f354d Binary files /dev/null and b/img/dark_logo@2x.png differ diff --git a/img/icon.png b/img/icon.png new file mode 100644 index 0000000..55fda16 Binary files /dev/null and b/img/icon.png differ diff --git a/img/icon@2x.png b/img/icon@2x.png new file mode 100644 index 0000000..d760759 Binary files /dev/null and b/img/icon@2x.png differ diff --git a/img/logo.png b/img/logo.png new file mode 100644 index 0000000..e5ee774 Binary files /dev/null and b/img/logo.png differ diff --git a/img/logo@2x.png b/img/logo@2x.png new file mode 100644 index 0000000..64b509a Binary files /dev/null and b/img/logo@2x.png differ diff --git a/info.md b/info.md index a1c6842..a9bf3c3 100644 --- a/info.md +++ b/info.md @@ -24,7 +24,7 @@ ## Installation 1. Click install. -1. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Bermuda BLE Triangulation". +1. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Bermuda BLE Trilateration". {% endif %} diff --git a/requirements_dev.txt b/requirements_dev.txt new file mode 100644 index 0000000..daefbd0 --- /dev/null +++ b/requirements_dev.txt @@ -0,0 +1,2 @@ +-r requirements.txt +-r requirements_test.txt diff --git a/scripts/develop b/scripts/develop index d7c4c2d..13d7e18 100755 --- a/scripts/develop +++ b/scripts/develop @@ -4,12 +4,12 @@ set -e cd "$(dirname "$0")/.." -if [[ ! -f venv/bin/activate ]]; then - echo "ERROR: No venv, make sure you run setup first!" - exit 1 -fi - -source venv/bin/activate +#if [[ ! -f venv/bin/activate ]]; then +# echo "ERROR: No venv, make sure you run setup first!" +# exit 1 +#fi +# +#source venv/bin/activate # Create config dir if not present diff --git a/scripts/lint b/scripts/lint index 04d0641..aad67aa 100755 --- a/scripts/lint +++ b/scripts/lint @@ -4,7 +4,7 @@ set -e cd "$(dirname "$0")/.." -source venv/bin/activate +#source venv/bin/activate #ruff check . --fix diff --git a/scripts/setup b/scripts/setup index 3909a2d..b95baff 100755 --- a/scripts/setup +++ b/scripts/setup @@ -5,8 +5,15 @@ set -e cd "$(dirname "$0")/.." # Create a virtual environment -python3 -m venv venv -source venv/bin/activate +# AJG - actually, don't. Thought it was a good idea but it breaks +# code completion etc because it seems vscode doesn't/won't run in +# that context. Or I'm an idiot, just as likely. +# +# Note that this setup script gets run on container init so everything +# should be ready to go on loading. +# +#python3 -m venv venv +#source venv/bin/activate # Seems to fix broken aiohttp wheel building in 3.11: python3 -m pip install --upgrade setuptools wheel diff --git a/scripts/test b/scripts/test index 15ea793..cec81ac 100755 --- a/scripts/test +++ b/scripts/test @@ -1,8 +1,8 @@ #!/bin/bash # Create a virtual environment -python3 -m venv venv -source venv/bin/activate +#python3 -m venv venv +#source venv/bin/activate # Install requirements pip install -r requirements_test.txt # Run tests and get a summary of successes/failures and code coverage diff --git a/tests/__init__.py b/tests/__init__.py index fe974bf..d2559b4 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1 @@ -"""Tests for Bermuda BLE Triangulation integration.""" +"""Tests for Bermuda BLE Trilateration integration.""" diff --git a/tests/conftest.py b/tests/conftest.py index a39fa4b..6b3e58b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,4 @@ -"""Global fixtures for Bermuda BLE Triangulation integration.""" +"""Global fixtures for Bermuda BLE Trilateration integration.""" from unittest.mock import patch import pytest diff --git a/tests/const.py b/tests/const.py index 43c5651..0e1bd7f 100644 --- a/tests/const.py +++ b/tests/const.py @@ -1,4 +1,4 @@ -"""Constants for Bermuda BLE Triangulation tests.""" +"""Constants for Bermuda BLE Trilateration tests.""" from custom_components.bermuda.const import ( CONF_PASSWORD, ) diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index dab1f4a..f7ca6ee 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -1,4 +1,4 @@ -"""Test Bermuda BLE Triangulation config flow.""" +"""Test Bermuda BLE Trilateration config flow.""" from unittest.mock import patch import pytest diff --git a/tests/test_init.py b/tests/test_init.py index 9a209c9..f583741 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,4 +1,4 @@ -"""Test Bermuda BLE Triangulation setup process.""" +"""Test Bermuda BLE Trilateration setup process.""" import pytest from custom_components.bermuda import ( async_reload_entry, diff --git a/tests/test_switch.py b/tests/test_switch.py index 0e0ea2d..6da17de 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -1,4 +1,4 @@ -"""Test Bermuda BLE Triangulation switch.""" +"""Test Bermuda BLE Trilateration switch.""" from unittest.mock import call from unittest.mock import patch