diff --git a/custom_components/bermuda/bermuda_device.py b/custom_components/bermuda/bermuda_device.py index 7bbb6bc..9774fa4 100644 --- a/custom_components/bermuda/bermuda_device.py +++ b/custom_components/bermuda/bermuda_device.py @@ -54,6 +54,7 @@ def __init__(self, address, options) -> None: self.local_name: str | None = None self.prefname: str | None = None # "preferred" name - ideally local_name self.address: str = address + self.ref_power: float = 0 # If non-zero, use in place of global ref_power. self.options = options self.unique_id: str | None = None # mac address formatted. self.address_type = BDADDR_TYPE_UNKNOWN @@ -78,6 +79,9 @@ def __init__(self, address, options) -> None: self.create_sensor: bool = False # Create/update a sensor for this device self.create_sensor_done: bool = False # Sensor should now exist self.create_tracker_done: bool = False # device_tracker should now exist + self.create_number_done: bool = False + self.create_button_done: bool = False + self.create_all_done: bool = False # All platform entities are done and ready. self.last_seen: float = 0 # stamp from most recent scanner spotting. MONOTONIC_TIME self.scanners: dict[str, BermudaDeviceScanner] = {} @@ -124,6 +128,54 @@ def __init__(self, address, options) -> None: else: self.address_type = BDADDR_TYPE_OTHER + def set_ref_power(self, value: float): + """ + Set a new reference power for this device and immediately apply + an interim distance calculation. + """ + self.ref_power = value + nearest_distance = 9999 # running tally to find closest scanner + nearest_scanner = None + for scanner in self.scanners.values(): + rawdist = scanner.set_ref_power(value) + if rawdist < nearest_distance: + nearest_distance = rawdist + nearest_scanner = scanner + if nearest_scanner is not None: + self.apply_scanner_selection(nearest_scanner) + + def apply_scanner_selection(self, closest_scanner: BermudaDeviceScanner | None): + """ + Given a DeviceScanner entry, apply the distance and area attributes + from it to this device. + + Used to apply a "winning" scanner's data to the device for setting closest Area. + """ + if closest_scanner is not None: + # We found a winner + old_area = self.area_name + self.area_id = closest_scanner.area_id + self.area_name = closest_scanner.area_name + self.area_distance = closest_scanner.rssi_distance + self.area_rssi = closest_scanner.rssi + self.area_scanner = closest_scanner.name + if (old_area != self.area_name) and self.create_sensor: + # We check against area_name so we can know if the + # device's area changed names. + _LOGGER.debug( + "Device %s was in '%s', now in '%s'", + self.name, + old_area, + self.area_name, + ) + else: + # Not close to any scanners! + self.area_id = None + self.area_name = None + self.area_distance = None + self.area_rssi = None + self.area_scanner = None + def calculate_data(self): """ Call after doing update_scanner() calls so that distances @@ -166,19 +218,21 @@ def update_scanner(self, scanner_device: BermudaDevice, discoveryinfo: Bluetooth if format_mac(scanner_device.address) in self.scanners: # Device already exists, update it self.scanners[format_mac(scanner_device.address)].update_advertisement( - self.address, discoveryinfo, # the entire BluetoothScannerDevice struct - scanner_device.area_id or "area_not_defined", ) + device_scanner = self.scanners[format_mac(scanner_device.address)] else: + # Create it self.scanners[format_mac(scanner_device.address)] = BermudaDeviceScanner( - self.address, + self, discoveryinfo, # the entire BluetoothScannerDevice struct - scanner_device.area_id or "area_not_defined", self.options, scanner_device, ) - device_scanner = self.scanners[format_mac(scanner_device.address)] + device_scanner = self.scanners[format_mac(scanner_device.address)] + # On first creation, we also want to copy our ref_power to it (but not afterwards, + # since a metadevice might take over that role later) + device_scanner.ref_power = self.ref_power # Let's see if we should update our last_seen based on this... if device_scanner.stamp is not None and self.last_seen < device_scanner.stamp: self.last_seen = device_scanner.stamp diff --git a/custom_components/bermuda/bermuda_device_scanner.py b/custom_components/bermuda/bermuda_device_scanner.py index ba0eed9..ba21945 100644 --- a/custom_components/bermuda/bermuda_device_scanner.py +++ b/custom_components/bermuda/bermuda_device_scanner.py @@ -9,7 +9,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from homeassistant.components.bluetooth import MONOTONIC_TIME, BluetoothScannerDevice @@ -37,20 +37,25 @@ class BermudaDeviceScanner(dict): """ Represents details from a scanner relevant to a specific device. - A BermudaDevice will contain 0 or more of these depending on whether - it has been "seen" by that scanner. + Effectively a link between two BermudaDevices, being the tracked device + and the scanner device. So each transmitting device will have a collection + of these BermudaDeviceScanner entries, one for each scanner that has picked + up the advertisement. + + This is created (and updated) by the receipt of an advertisement, which represents + a BermudaDevice hearing an advert from another BermudaDevice, if that makes sense! + + A BermudaDevice's "scanners" property will contain one of these for each + scanner that has "seen" it. - Note that details on a scanner itself are BermudaDevice instances - in their own right. """ def __init__( self, - device_address: str, - scandata: BluetoothScannerDevice, - area_id: str, + parent_device: BermudaDevice, # The device being tracked + scandata: BluetoothScannerDevice, # The advertisement info from the device, received by the scanner options, - scanner_device: BermudaDevice, + scanner_device: BermudaDevice, # The scanner device that "saw" it. ) -> None: # I am declaring these just to control their order in the dump, # which is a bit silly, I suspect. @@ -59,28 +64,38 @@ def __init__( self.adapter: str = scandata.scanner.adapter self.address = scanner_device.address self.source: str = scandata.scanner.source - self.area_id: str = area_id - self.parent_device = device_address + self.area_id: str | None = scanner_device.area_id + self.area_name: str | None = scanner_device.area_name + self.parent_device = parent_device + self.parent_device_address = parent_device.address + self.scanner_device = scanner_device # links to the source device self.options = options self.stamp: float | None = 0 + self.scanner_sends_stamps: bool = False self.new_stamp: float | None = None # Set when a new advert is loaded from update - self.hist_stamp = [] self.rssi: float | None = None + self.tx_power: float | None = None + self.rssi_distance: float | None = None + self.rssi_distance_raw: float | None = None + self.ref_power: float = 0 # Override of global, set from parent device. + self.stale_update_count = 0 # How many times we did an update but no new stamps were found. + self.hist_stamp = [] self.hist_rssi = [] self.hist_distance = [] self.hist_distance_by_interval = [] # updated per-interval self.hist_interval = [] # WARNING: This is actually "age of ad when we polled" self.hist_velocity = [] # Effective velocity versus previous stamped reading - self.stale_update_count = 0 # How many times we did an update but no new stamps were found. - self.tx_power: float | None = None - self.rssi_distance: float | None = None - self.rssi_distance_raw: float | None = None - self.adverts: dict[str, bytes] = {} + self.adverts: dict[str, list] = { + "manufacturer_data": [], + "service_data": [], + "service_uuids": [], + "platform_data": [], + } # Just pass the rest on to update... - self.update_advertisement(device_address, scandata, area_id) + self.update_advertisement(scandata) - def update_advertisement(self, device_address: str, scandata: BluetoothScannerDevice, area_id: str): + def update_advertisement(self, scandata: BluetoothScannerDevice): """ Update gets called every time we see a new packet or every time we do a polled update. @@ -90,9 +105,10 @@ def update_advertisement(self, device_address: str, scandata: BluetoothScannerDe claims to have data. """ # In case the scanner has changed it's details since startup: - self.name: str = scandata.scanner.name - self.area_id: str = area_id - new_stamp: float | None = None + self.name = scandata.scanner.name + self.area_id = self.scanner_device.area_id + self.area_name = self.scanner_device.area_name + new_stamp = None # Only remote scanners log timestamps here (local usb adaptors do not), if hasattr(scandata.scanner, "_discovered_device_timestamps"): @@ -104,7 +120,7 @@ def update_advertisement(self, device_address: str, scandata: BluetoothScannerDe stamps = scandata.scanner._discovered_device_timestamps # type: ignore #noqa # In this dict all MAC address keys are upper-cased - uppermac = device_address.upper() + uppermac = self.parent_device_address.upper() if uppermac in stamps: if self.stamp is None or (stamps[uppermac] is not None and stamps[uppermac] > self.stamp): new_stamp = stamps[uppermac] @@ -118,7 +134,7 @@ def update_advertisement(self, device_address: str, scandata: BluetoothScannerDe _LOGGER.error( "Scanner %s has no stamp for %s - very odd.", scandata.scanner.source, - device_address, + self.parent_device_address, ) new_stamp = None else: @@ -140,16 +156,13 @@ def update_advertisement(self, device_address: str, scandata: BluetoothScannerDe new_stamp = None if len(self.hist_stamp) == 0 or new_stamp is not None: - # this is the first entry or a new one... + # this is the first entry or a new one, bring in the new reading + # and calculate the distance. self.rssi = scandata.advertisement.rssi self.hist_rssi.insert(0, self.rssi) - self.rssi_distance_raw = rssi_to_metres( - self.rssi + self.options.get(CONF_RSSI_OFFSETS, {}).get(self.address, 0), - self.options.get(CONF_REF_POWER), - self.options.get(CONF_ATTENUATION), - ) - self.hist_distance.insert(0, self.rssi_distance_raw) + + self._update_raw_distance(reading_is_new=True) # Note: this is not actually the interval between adverts, # but rather a function of our UPDATE_INTERVAL plus the packet @@ -180,15 +193,70 @@ def update_advertisement(self, device_address: str, scandata: BluetoothScannerDe # Changing from warning to debug to quiet users' logs. _LOGGER.debug( "Device changed TX-POWER! That was unexpected: %s %sdB", - device_address, + self.parent_device_address, scandata.advertisement.tx_power, ) self.tx_power = scandata.advertisement.tx_power - for ad_str, ad_bytes in scandata.advertisement.service_data.items(): - self.adverts[ad_str] = ad_bytes + + # Track each advertisement element as or if they change. + for key, data in self.adverts.items(): + new_data = getattr(scandata.advertisement, key, {}) + if len(new_data) > 0: + if len(data) == 0 or data[0] != new_data: + data.insert(0, new_data) + # trim to keep size in check + del data[HIST_KEEP_COUNT:] self.new_stamp = new_stamp + def _update_raw_distance(self, reading_is_new=True) -> float: + """ + Converts rssi to raw distance and updates history stack and + returns the new raw distance. + + reading_is_new should only be called by the regular update + cycle, as it creates a new entry in the histories. Call with + false if you just need to set / override distance measurements + immediately, perhaps between cycles, in order to reflect a + setting change (such as altering a device's ref_power setting). + """ + # Check if we should use a device-based ref_power + if self.ref_power == 0: + ref_power = self.options.get(CONF_REF_POWER) + else: + ref_power = self.ref_power + + distance = rssi_to_metres( + self.rssi + self.options.get(CONF_RSSI_OFFSETS, {}).get(self.address, 0), + ref_power, + self.options.get(CONF_ATTENUATION), + ) + self.rssi_distance_raw = distance + if reading_is_new: + # Add a new historical reading + self.hist_distance.insert(0, distance) + # don't insert into hist_distance_by_interval, that's done by the caller. + else: + # We are over-riding readings between cycles. Force the + # new value in-place. + self.rssi_distance = distance + if len(self.hist_distance) > 0: + self.hist_distance[0] = distance + else: + self.hist_distance.append(distance) + if len(self.hist_distance_by_interval) > 0: + self.hist_distance_by_interval[0] = distance + # We don't else because we don't want to *add* a hist-by-interval reading, only + # modify in-place. + return distance + + def set_ref_power(self, value: float): + """Set a new reference power from the parent device and immediately update distance.""" + # When the user updates the ref_power we want to reflect that change immediately, + # and not subject it to the normal smoothing algo. + self.ref_power = value + return self._update_raw_distance(False) + def calculate_data(self): """ Filter and update distance estimates. @@ -293,9 +361,7 @@ def calculate_data(self): # (not so for == 0 since it might still be an invalid retreat) break - if velocity > peak_velocity: - # but on subsequent comparisons we only care if they're faster retreats - peak_velocity = velocity + peak_velocity = max(velocity, peak_velocity) # we've been through the history and have peak velo retreat, or the most recent # approach velo. velocity = peak_velocity @@ -306,10 +372,10 @@ def calculate_data(self): self.hist_velocity.insert(0, velocity) if velocity > self.options.get(CONF_MAX_VELOCITY): - if self.parent_device.upper() in self.options.get(CONF_DEVICES, []): + if self.parent_device_address.upper() in self.options.get(CONF_DEVICES, []): _LOGGER.debug( "This sparrow %s flies too fast (%2fm/s), ignoring", - self.parent_device, + self.parent_device_address, velocity, ) # Discard the bogus reading by duplicating the last. @@ -366,10 +432,21 @@ def to_dict(self): """Convert class to serialisable dict for dump_devices.""" out = {} for var, val in vars(self).items(): + if var in ["options", "parent_device", "scanner_device"]: + # skip certain vars that we don't want in the dump output. + continue if var == "adverts": - # FIXME: val is overwritten in loop - val = {} # noqa - for uuid, thebytes in self.adverts.items(): - val[uuid] = thebytes.hex() + adout = {} + for adtype, adarray in val.items(): + out_adarray = [] + for ad_data in adarray: + if adtype in ["manufacturer_data", "service_data"]: + for ad_key, ad_value in ad_data.items(): + out_adarray.append({ad_key: cast(bytes, ad_value).hex()}) + else: + out_adarray.append(ad_data) + adout[adtype] = out_adarray + out[var] = adout + continue out[var] = val return out diff --git a/custom_components/bermuda/const.py b/custom_components/bermuda/const.py index a9f59f2..5626247 100644 --- a/custom_components/bermuda/const.py +++ b/custom_components/bermuda/const.py @@ -29,11 +29,13 @@ # Platforms BINARY_SENSOR = "binary_sensor" +BUTTON = "button" SENSOR = "sensor" SWITCH = "switch" DEVICE_TRACKER = "device_tracker" +NUMBER = "number" # PLATFORMS = [BINARY_SENSOR, SENSOR, SWITCH] -PLATFORMS = [SENSOR, DEVICE_TRACKER] +PLATFORMS = [SENSOR, DEVICE_TRACKER, NUMBER] # Should probably retreive this from the component, but it's in "DOMAIN" *shrug* DOMAIN_PRIVATE_BLE_DEVICE = "private_ble_device" diff --git a/custom_components/bermuda/coordinator.py b/custom_components/bermuda/coordinator.py index 710f31b..1da14a3 100644 --- a/custom_components/bermuda/coordinator.py +++ b/custom_components/bermuda/coordinator.py @@ -312,6 +312,7 @@ def handle_devreg_changes(ev: Event[EventDeviceRegistryUpdatedData]): setattr(scanner, key, value) self.scanner_list.append(address) + # Set up the dump_devices service hass.services.async_register( DOMAIN, "dump_devices", @@ -368,6 +369,13 @@ def async_handle_advert( if self.stamp_last_update < MONOTONIC_TIME() - (UPDATE_INTERVAL * 2): self.hass.add_job(self._async_update_data()) + def _check_all_platforms_created(self, address): + """Checks if all platforms have finished loading a device's entities.""" + dev = self._get_device(address) + if dev is not None: + if all([dev.create_sensor_done, dev.create_tracker_done, dev.create_number_done]): + dev.create_all_done = True + def sensor_created(self, address): """Allows sensor platform to report back that sensors have been set up.""" dev = self._get_device(address) @@ -376,6 +384,7 @@ def sensor_created(self, address): # _LOGGER.debug("Sensor confirmed created for %s", address) else: _LOGGER.warning("Very odd, we got sensor_created for non-tracked device") + self._check_all_platforms_created(address) def device_tracker_created(self, address): """Allows device_tracker platform to report back that sensors have been set up.""" @@ -385,6 +394,21 @@ def device_tracker_created(self, address): # _LOGGER.debug("Device_tracker confirmed created for %s", address) else: _LOGGER.warning("Very odd, we got sensor_created for non-tracked device") + self._check_all_platforms_created(address) + + def number_created(self, address): + """Receives report from number platform that sensors have been set up.""" + dev = self._get_device(address) + if dev is not None: + dev.create_number_done = True + self._check_all_platforms_created(address) + + # def button_created(self, address): + # """Receives report from number platform that sensors have been set up.""" + # dev = self._get_device(address) + # if dev is not None: + # dev.create_button_done = True + # self._check_all_platforms_created(address) def count_active_devices(self) -> int: """ @@ -630,7 +654,7 @@ async def _async_update_data(self): # already loaded up. for address, device in self.devices.items(): if device.create_sensor: - if not device.create_sensor_done or not device.create_tracker_done: + if not device.create_all_done: _LOGGER.debug("Firing device_new for %s (%s)", device.name, address) # Note that the below should be OK thread-wise, debugger indicates this is being # called by _run in events.py, so pretty sure we are "in the event loop". @@ -871,6 +895,9 @@ def update_metadevices(self): Note that at this point all the distances etc should be fresh for the source devices, so we can just copy values from them to the metadevice. + However, the sources might not yet be using the metadevice's custom ref_power, + so their *first* update might have the un-adjusted value after a mac change or + other initialisation. """ # First seed the metadevice skeletons and set their latest beacon_source entries # Private BLE Devices: @@ -892,6 +919,9 @@ def update_metadevices(self): # Map the source device's scanner list into ours metadev.scanners = source_device.scanners + # Set the source device's ref_power from our own + source_device.set_ref_power(metadev.ref_power) + # anything that isn't already set to something interesting, overwrite # it with the new device's data. # Defaults: @@ -969,6 +999,18 @@ def dt_mono_to_age(self, stamp) -> str: """Convert monotonic timestamp to age (eg: "6 seconds ago").""" return get_age(self.dt_mono_to_datetime(stamp)) + def resolve_area_name(self, area_id) -> str | None: + """ + Given an area_id, return the current area name. + + Will return None if the area id does *not* resolve to a single + known area name. + """ + areas = self.area_reg.async_get_area(area_id) + if hasattr(areas, "name"): + return getattr(areas, "name", "invalid_area") + return None + def _refresh_areas_by_min_distance(self): """Set area for ALL devices based on closest beacon.""" for device in self.devices.values(): @@ -995,43 +1037,8 @@ def _refresh_area_by_min_distance(self, device: BermudaDevice): # We're closer than the last-closest, we win! closest_scanner = scanner - if closest_scanner is not None: - # We found a winner - old_area = device.area_name - device.area_id = closest_scanner.area_id - areas = self.area_reg.async_get_area(device.area_id) - if hasattr(areas, "name"): - device.area_name = getattr(areas, "name", "invalid_area") - else: - # Wasn't a single area entry. Let's freak out, but not in a spammy way. - _LOGGER_SPAM_LESS.warning( - f"scanner_no_area_{closest_scanner.name}", - "Could not discern area from scanner %s: %s." - "Please assign an area then reload this integration" - "- Bermuda can't really work without it.", - closest_scanner.name, - areas, - ) - device.area_name = f"No area: {closest_scanner.name}" - device.area_distance = closest_scanner.rssi_distance - device.area_rssi = closest_scanner.rssi - device.area_scanner = closest_scanner.name - if (old_area != device.area_name) and device.create_sensor: - # We check against area_name so we can know if the - # device's area changed names. - _LOGGER.debug( - "Device %s was in '%s', now in '%s'", - device.name, - old_area, - device.area_name, - ) - else: - # Not close to any scanners! - device.area_id = None - device.area_name = None - device.area_distance = None - device.area_rssi = None - device.area_scanner = None + # Apply the newly-found closest scanner (or apply None if we didn't find one) + device.apply_scanner_selection(closest_scanner) def _refresh_scanners(self, scanners: list[BluetoothScannerDevice] | None = None): """ diff --git a/custom_components/bermuda/entity.py b/custom_components/bermuda/entity.py index d8e6dbb..cc5e98f 100644 --- a/custom_components/bermuda/entity.py +++ b/custom_components/bermuda/entity.py @@ -43,6 +43,7 @@ def __init__( super().__init__(coordinator) self.coordinator = coordinator self.config_entry = config_entry + self.address = address self._device = coordinator.devices[address] self.area_reg = ar.async_get(coordinator.hass) self.devreg = dr.async_get(coordinator.hass) diff --git a/custom_components/bermuda/number.py b/custom_components/bermuda/number.py new file mode 100644 index 0000000..4587b96 --- /dev/null +++ b/custom_components/bermuda/number.py @@ -0,0 +1,146 @@ +"""Create Number entities - like per-device rssi ref_power, etc.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from homeassistant.components.number import ( + NumberDeviceClass, + NumberExtraStoredData, + NumberMode, + RestoreNumber, +) +from homeassistant.const import SIGNAL_STRENGTH_DECIBELS_MILLIWATT, EntityCategory +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import SIGNAL_DEVICE_NEW +from .entity import BermudaEntity + +if TYPE_CHECKING: + from homeassistant.helpers.entity_platform import AddEntitiesCallback + + from . import BermudaConfigEntry + from .coordinator import BermudaDataUpdateCoordinator + + +async def async_setup_entry( + hass: HomeAssistant, + entry: BermudaConfigEntry, + async_add_devices: AddEntitiesCallback, +) -> None: + """Load Number entities for a config entry.""" + coordinator: BermudaDataUpdateCoordinator = entry.runtime_data.coordinator + + created_devices = [] # list of devices we've already created entities for + + @callback + def device_new(address: str, scanners: list[str]) -> None: # pylint: disable=unused-argument + """ + Create entities for newly-found device. + + Called from the data co-ordinator when it finds a new device that needs + to have sensors created. Not called directly, but via the dispatch + facility from HA. + Make sure you have a full list of scanners ready before calling this. + """ + if address not in created_devices: + entities = [] + entities.append(BermudaNumber(coordinator, entry, address)) + # We set update before add to False because we are being + # call(back(ed)) from the update, so causing it to call another would be... bad. + async_add_devices(entities, False) + created_devices.append(address) + else: + # _LOGGER.debug( + # "Ignoring create request for existing dev_tracker %s", address + # ) + pass + # tell the co-ord we've done it. + coordinator.number_created(address) + + # Connect device_new to a signal so the coordinator can call it + entry.async_on_unload(async_dispatcher_connect(hass, SIGNAL_DEVICE_NEW, device_new)) + + # Now we must tell the co-ord to do initial refresh, so that it will call our callback. + # await coordinator.async_config_entry_first_refresh() + + +class BermudaNumber(BermudaEntity, RestoreNumber): + """A Number entity for bermuda devices.""" + + _attr_should_poll = False + _attr_has_entity_name = True + _attr_name = "Calibration Ref Power at 1m. 0 for default." + _attr_translation_key = "ref_power" + _attr_device_class = NumberDeviceClass.SIGNAL_STRENGTH + _attr_entity_category = EntityCategory.CONFIG + # _attr_entity_registry_enabled_default = False + _attr_native_min_value = -127 + _attr_native_max_value = 0 + _attr_native_step = 1 + _attr_native_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT + _attr_mode = NumberMode.BOX + + def __init__( + self, + coordinator: BermudaDataUpdateCoordinator, + entry: BermudaConfigEntry, + address: str, + ) -> None: + """Initialise the number entity.""" + self.restored_data: NumberExtraStoredData | None = None + super().__init__(coordinator, entry, address) + + async def async_added_to_hass(self) -> None: + """Restore values from HA storage on startup.""" + await super().async_added_to_hass() + self.restored_data = await self.async_get_last_number_data() + if self.restored_data is not None and self.restored_data.native_value is not None: + self.coordinator.devices[self.address].set_ref_power(self.restored_data.native_value) + + @property + def native_value(self) -> float | None: + """Return value of number.""" + # if self.restored_data is not None and self.restored_data.native_value is not None: + # return self.restored_data.native_value + return self.coordinator.devices[self.address].ref_power + return 0 + + async def async_set_native_value(self, value: float) -> None: + """Set value.""" + self.coordinator.devices[self.address].set_ref_power(value) + self.async_write_ha_state() + # Beware that STATE_DUMP_INTERVAL for restore_state's dump_state + # is 15 minutes, so if HA is killed instead of exiting cleanly, + # updated values may not be restored. Tempting to schedule a dump + # here, since updates to calib will be infrequent, but users are + # moderately likely to restart HA after playing with them. + + @property + def unique_id(self): + """ + "Uniquely identify this sensor so that it gets stored in the entity_registry, + and can be maintained / renamed etc by the user. + """ + return f"{self._device.unique_id}_ref_power" + + # @property + # def extra_state_attributes(self) -> Mapping[str, Any]: + # """Return extra state attributes for this device.""" + # return {"scanner": self._device.area_scanner, "area": self._device.area_name} + + # @property + # def state(self) -> str: + # """Return the state of the device.""" + # return self._device.zone + + # @property + # def source_type(self) -> SourceType: + # """Return the source type, eg gps or router, of the device.""" + # return SourceType.BLUETOOTH_LE + + # @property + # def icon(self) -> str: + # """Return device icon.""" + # return "mdi:bluetooth-connect" if self._device.zone == STATE_HOME else "mdi:bluetooth-off"