1 """Read temperature information from Eddystone beacons.
3 Your beacons must be configured to transmit UID (for identification) and TLM
4 (for temperature) frames.
7 from __future__
import annotations
11 from beacontools
import BeaconScanner, EddystoneFilter, EddystoneTLMFrame
12 import voluptuous
as vol
15 PLATFORM_SCHEMA
as SENSOR_PLATFORM_SCHEMA,
21 EVENT_HOMEASSISTANT_START,
22 EVENT_HOMEASSISTANT_STOP,
31 _LOGGER = logging.getLogger(__name__)
33 CONF_BEACONS =
"beacons"
34 CONF_BT_DEVICE_ID =
"bt_device_id"
35 CONF_INSTANCE =
"instance"
36 CONF_NAMESPACE =
"namespace"
38 BEACON_SCHEMA = vol.Schema(
40 vol.Required(CONF_NAMESPACE): cv.string,
41 vol.Required(CONF_INSTANCE): cv.string,
42 vol.Optional(CONF_NAME): cv.string,
46 PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
48 vol.Optional(CONF_BT_DEVICE_ID, default=0): cv.positive_int,
49 vol.Required(CONF_BEACONS): vol.Schema({cv.string: BEACON_SCHEMA}),
57 add_entities: AddEntitiesCallback,
58 discovery_info: DiscoveryInfoType |
None =
None,
60 """Validate configuration, create devices and start monitoring thread."""
61 bt_device_id: int = config[CONF_BT_DEVICE_ID]
63 beacons: dict[str, dict[str, str]] = config[CONF_BEACONS]
64 devices: list[EddystoneTemp] = []
66 for dev_name, properties
in beacons.items():
69 name = properties.get(CONF_NAME, dev_name)
71 if instance
is None or namespace
is None:
72 _LOGGER.error(
"Skipping %s", dev_name)
78 mon =
Monitor(hass, devices, bt_device_id)
80 def monitor_stop(event: Event) ->
None:
81 """Stop the monitor thread."""
82 _LOGGER.debug(
"Stopping scanner for Eddystone beacons")
85 def monitor_start(event: Event) ->
None:
86 """Start the monitor thread."""
87 _LOGGER.debug(
"Starting scanner for Eddystone beacons")
92 hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, monitor_stop)
93 hass.bus.listen_once(EVENT_HOMEASSISTANT_START, monitor_start)
95 _LOGGER.warning(
"No devices were added")
98 def get_from_conf(config: dict[str, str], config_key: str, length: int) -> str |
None:
99 """Retrieve value from config and validate length."""
100 string = config[config_key]
101 if len(string) != length:
104 "Error in configuration parameter %s: Must be exactly %d "
105 "bytes. Device will not be added"
115 """Representation of a temperature sensor."""
117 _attr_device_class = SensorDeviceClass.TEMPERATURE
118 _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
119 _attr_should_poll =
False
121 def __init__(self, name: str, namespace: str, instance: str) ->
None:
122 """Initialize a sensor."""
131 """Return the state of the device."""
136 """Continuously scan for BLE advertisements."""
139 self, hass: HomeAssistant, devices: list[EddystoneTemp], bt_device_id: int
141 """Construct interface object."""
149 def callback(bt_addr, _, packet, additional_info):
150 """Handle new packets."""
152 additional_info[
"namespace"],
153 additional_info[
"instance"],
157 device_filters = [EddystoneFilter(d.namespace, d.instance)
for d
in devices]
160 callback, bt_device_id, device_filters, EddystoneTLMFrame
165 """Continuously scan for BLE advertisements."""
170 _LOGGER.debug(
"start() called, but scanner is already running")
173 """Assign temperature to device."""
175 "Received temperature for <%s,%s>: %d", namespace, instance, temperature
178 for dev
in self.
devicesdevices:
180 dev.namespace == namespace
181 and dev.instance == instance
182 and dev.temperature != temperature
184 dev.temperature = temperature
185 dev.schedule_update_ha_state()
188 """Signal runner to stop and join thread."""
190 _LOGGER.debug(
"Stopping")
192 _LOGGER.debug(
"Stopped")
195 _LOGGER.debug(
"stop() called but scanner was not running")
None __init__(self, str name, str namespace, str instance)
None __init__(self, HomeAssistant hass, list[EddystoneTemp] devices, int bt_device_id)
None process_packet(self, namespace, instance, temperature)
str|None get_from_conf(dict[str, str] config, str config_key, int length)
None setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback add_entities, DiscoveryInfoType|None discovery_info=None)
def add_entities(account, async_add_entities, tracked)