From 5e3a727a8334625d3a63264ae9023e33f79cfa0b Mon Sep 17 00:00:00 2001 From: PeteBa Date: Sun, 31 Jan 2021 11:52:13 +0000 Subject: [PATCH] feat(home-assistant): add device tracker discovery (#425) --- docs/integrations/home-assistant.md | 14 --------- src/entities/device-tracker.ts | 2 +- .../bluetooth-low-energy.service.spec.ts | 20 +++++++++++-- .../bluetooth-low-energy.service.ts | 24 ++++++++++++--- .../home-assistant/device-tracker-config.ts | 10 +++++-- .../home-assistant.service.spec.ts | 29 +++++++++++++++++-- .../home-assistant/home-assistant.service.ts | 29 +++++++------------ .../room-presence-distance.sensor.ts | 1 + .../room-presence/room-presence.proxy.spec.ts | 2 +- .../room-presence/room-presence.proxy.ts | 4 ++- 10 files changed, 89 insertions(+), 46 deletions(-) diff --git a/docs/integrations/home-assistant.md b/docs/integrations/home-assistant.md index a14b26c..07b19ab 100644 --- a/docs/integrations/home-assistant.md +++ b/docs/integrations/home-assistant.md @@ -16,20 +16,6 @@ You will need to setup an MQTT broker that both your instance of Home Assistant room-assistant makes use of the [MQTT auto discovery](https://www.home-assistant.io/docs/mqtt/discovery/) features provided by Home Assistant Core to automatically create all entities for you. It is strongly recommended that you enable this feature when setting up the MQTT integration in Home Assistant Core. -## Device Trackers - -Some integrations, such as [Bluetooth Classic](bluetooth-classic.md) or [Bluetooth Low Energy](bluetooth-low-energy.md) support the [MQTT device tracker](https://www.home-assistant.io/integrations/device_tracker.mqtt/) in Home Assistant Core. This is for example useful if you want to integrate the room-assistant presence detection with others in the form of a [person](https://www.home-assistant.io/integrations/person/). Unfortunately, device trackers can not be auto discovered by Home Assistant Core. To enable this feature you will have to edit your Home Assistant Core configuration manually and add a few lines as shown in the example below. You can get all your available device tracker topics from the room-assistant logs. - -```yaml -device_tracker: -- platform: mqtt - devices: - apple_watch: 'room-assistant/device_tracker/bluetooth-classic-aa-aa-aa-aa-aa-aa-tracker/state' - payload_home: 'true' - payload_not_home: 'false' - source_type: bluetooth -``` - ## Settings | Name | Type | Default | Description | diff --git a/src/entities/device-tracker.ts b/src/entities/device-tracker.ts index 861992e..0e95ef7 100644 --- a/src/entities/device-tracker.ts +++ b/src/entities/device-tracker.ts @@ -1,5 +1,5 @@ import { Entity } from './entity.dto'; export class DeviceTracker extends Entity { - state: boolean; + state: string; } diff --git a/src/integrations/bluetooth-low-energy/bluetooth-low-energy.service.spec.ts b/src/integrations/bluetooth-low-energy/bluetooth-low-energy.service.spec.ts index 186a360..e7817cb 100644 --- a/src/integrations/bluetooth-low-energy/bluetooth-low-energy.service.spec.ts +++ b/src/integrations/bluetooth-low-energy/bluetooth-low-energy.service.spec.ts @@ -33,6 +33,7 @@ import { NewDistanceEvent } from './new-distance.event'; import { BluetoothLowEnergyPresenceSensor } from './bluetooth-low-energy-presence.sensor'; import KalmanFilter from 'kalmanjs'; import { DeviceTracker } from '../../entities/device-tracker'; +import { DeviceTrackerConfig } from '../home-assistant/device-tracker-config'; import * as util from 'util'; import { BluetoothService } from '../bluetooth/bluetooth.service'; import { BluetoothModule } from '../bluetooth/bluetooth.module'; @@ -717,7 +718,7 @@ describe('BluetoothLowEnergyService', () => { expect(sensorHandleSpy).toHaveBeenCalledWith('test-instance', 2, false); }); - it('should add new room presence sensor if no matching ones exist yet', () => { + it('should add new room presence sensor and device tracker if no matching ones exist yet', () => { const sensor = new BluetoothLowEnergyPresenceSensor('test', 'Test', 0); entitiesService.has.mockReturnValue(false); entitiesService.add.mockReturnValue(sensor); @@ -737,7 +738,19 @@ describe('BluetoothLowEnergyService', () => { expect.any(Array) ); expect(entitiesService.add).toHaveBeenCalledWith( - new DeviceTracker('ble-new-tracker', 'New Tag Tracker', true) + new DeviceTracker('ble-new-tracker', 'New Tag Tracker', true), + [ + { + for: DeviceTrackerConfig, + overrides: { + device: { + identifiers: 'new', + name: 'New Tag', + viaDevice: 'room-assistant-distributed', + }, + }, + }, + ] ); expect( util.types.isProxy(entitiesService.add.mock.calls[1][0]) @@ -772,7 +785,8 @@ describe('BluetoothLowEnergyService', () => { expect.any(Array) ); expect(entitiesService.add).toHaveBeenCalledWith( - new DeviceTracker('ble-new-tracker', 'New Tag Tracker', true) + new DeviceTracker('ble-new-tracker', 'New Tag Tracker', true), + expect.any(Array) ); expect(entitiesService.add).toHaveBeenCalledWith( new Sensor('ble-new-battery', 'New Tag Battery', true), diff --git a/src/integrations/bluetooth-low-energy/bluetooth-low-energy.service.ts b/src/integrations/bluetooth-low-energy/bluetooth-low-energy.service.ts index 2be34df..0ac1b80 100644 --- a/src/integrations/bluetooth-low-energy/bluetooth-low-energy.service.ts +++ b/src/integrations/bluetooth-low-energy/bluetooth-low-energy.service.ts @@ -14,6 +14,7 @@ import { ClusterService } from '../../cluster/cluster.service'; import { NewDistanceEvent } from './new-distance.event'; import { EntityCustomization } from '../../entities/entity-customization.interface'; import { SensorConfig } from '../home-assistant/sensor-config'; +import { DeviceTrackerConfig } from '../home-assistant/device-tracker-config'; import { Device } from '../home-assistant/device'; import { RoomPresenceDistanceSensor } from '../room-presence/room-presence-distance.sensor'; import { SchedulerRegistry } from '@nestjs/schedule'; @@ -237,7 +238,7 @@ export class BluetoothLowEnergyService } /** - * Creates and registers a new room presence sensor. + * Creates and registers a new room presence sensor and device tracker. * * @param sensorId - Id that the sensor should receive * @param deviceId - Id of the BLE peripheral @@ -259,7 +260,8 @@ export class BluetoothLowEnergyService const deviceTracker = this.createDeviceTracker( makeId(`${sensorId}-tracker`), - `${deviceName} Tracker` + `${deviceName} Tracker`, + deviceInfo ); let batterySensor: Sensor; @@ -309,11 +311,25 @@ export class BluetoothLowEnergyService * * @param id - Entity ID for the new device tracker * @param name - Name for the new device tracker + * @param deviceInfo - Reference information about the BLE device * @returns Registered device tracker */ - protected createDeviceTracker(id: string, name: string): DeviceTracker { + protected createDeviceTracker( + id: string, + name: string, + deviceInfo: Device + ): DeviceTracker { + const trackerCustomizations: Array> = [ + { + for: DeviceTrackerConfig, + overrides: { + device: deviceInfo, + }, + }, + ]; return this.entitiesService.add( - new DeviceTracker(id, name, true) + new DeviceTracker(id, name, true), + trackerCustomizations ) as DeviceTracker; } diff --git a/src/integrations/home-assistant/device-tracker-config.ts b/src/integrations/home-assistant/device-tracker-config.ts index 6e1a952..30b40da 100644 --- a/src/integrations/home-assistant/device-tracker-config.ts +++ b/src/integrations/home-assistant/device-tracker-config.ts @@ -1,8 +1,14 @@ import { EntityConfig } from './entity-config'; +import { + STATE_HOME, + STATE_NOT_HOME, +} from '../room-presence/room-presence-distance.sensor'; + export class DeviceTrackerConfig extends EntityConfig { - readonly payloadHome = 'true'; - readonly payloadNotHome = 'false'; + readonly payloadHome = STATE_HOME; + readonly payloadNotHome = STATE_NOT_HOME; + readonly sourceType = 'bluetooth_le'; constructor(id: string, name: string) { super('device_tracker', id, name); diff --git a/src/integrations/home-assistant/home-assistant.service.spec.ts b/src/integrations/home-assistant/home-assistant.service.spec.ts index b08886d..b7d049b 100644 --- a/src/integrations/home-assistant/home-assistant.service.spec.ts +++ b/src/integrations/home-assistant/home-assistant.service.spec.ts @@ -308,13 +308,38 @@ describe('HomeAssistantService', () => { ); }); - it('should not publish discovery information for a new device tracker', async () => { + it('should publish discovery information for a new device tracker', async () => { await service.onModuleInit(); service.handleNewEntity( new DeviceTracker('test-tracker', 'Test Tracker', true) ); - expect(mockMqttClient.publish).toHaveBeenCalledTimes(1); + expect(mockMqttClient.publish).toHaveBeenCalledWith( + 'homeassistant/device_tracker/room-assistant/test-tracker/config', + expect.any(String), + { + qos: 0, + retain: true, + } + ); + expect(mockMqttClient.publish).toHaveBeenCalledWith( + 'room-assistant/device_tracker/test-tracker/status', + 'online', + { + qos: 0, + retain: true, + } + ); + expect(JSON.parse(mockMqttClient.publish.mock.calls[0][1])).toMatchObject({ + unique_id: 'room-assistant-test-tracker', + name: 'Test Tracker', + state_topic: 'room-assistant/device_tracker/test-tracker/state', + json_attributes_topic: + 'room-assistant/device_tracker/test-tracker/attributes', + availability_topic: 'room-assistant/device_tracker/test-tracker/status', + payload_home: 'home', + payload_not_home: 'not_home', + }); }); it('should publish discovery information for a new camera', async () => { diff --git a/src/integrations/home-assistant/home-assistant.service.ts b/src/integrations/home-assistant/home-assistant.service.ts index e0ea130..928da93 100644 --- a/src/integrations/home-assistant/home-assistant.service.ts +++ b/src/integrations/home-assistant/home-assistant.service.ts @@ -135,25 +135,18 @@ export class HomeAssistantService this.entityConfigs.set(combinedId, config); - if (config instanceof DeviceTrackerConfig) { - // auto discovery not supported by Home Assistant yet - this.logger.log( - `Device tracker requires manual setup in Home Assistant with topic: ${config.stateTopic}` - ); - } else { - // camera entities do not support stateTopic - const message = this.formatMessage( - config instanceof CameraConfig ? _.omit(config, ['stateTopic']) : config - ); + // camera entities do not support stateTopic + const message = this.formatMessage( + config instanceof CameraConfig ? _.omit(config, ['stateTopic']) : config + ); - this.logger.debug( - `Registering entity ${config.uniqueId} under ${config.configTopic}` - ); - this.mqttClient.publish(config.configTopic, JSON.stringify(message), { - qos: 0, - retain: true, - }); - } + this.logger.debug( + `Registering entity ${config.uniqueId} under ${config.configTopic}` + ); + this.mqttClient.publish(config.configTopic, JSON.stringify(message), { + qos: 0, + retain: true, + }); this.mqttClient.publish(config.availabilityTopic, config.payloadAvailable, { qos: 0, diff --git a/src/integrations/room-presence/room-presence-distance.sensor.ts b/src/integrations/room-presence/room-presence-distance.sensor.ts index fe2a327..cdb0172 100644 --- a/src/integrations/room-presence/room-presence-distance.sensor.ts +++ b/src/integrations/room-presence/room-presence-distance.sensor.ts @@ -1,5 +1,6 @@ import { Sensor } from '../../entities/sensor'; +export const STATE_HOME = 'home'; export const STATE_NOT_HOME = 'not_home'; class TimedDistance { diff --git a/src/integrations/room-presence/room-presence.proxy.spec.ts b/src/integrations/room-presence/room-presence.proxy.spec.ts index 806c96c..720381c 100644 --- a/src/integrations/room-presence/room-presence.proxy.spec.ts +++ b/src/integrations/room-presence/room-presence.proxy.spec.ts @@ -29,7 +29,7 @@ describe('RoomPresenceProxyHandler', () => { it('should set the device tracker to false when not home', () => { proxy.updateState(); - expect(deviceTracker.state).toBeFalsy(); + expect(deviceTracker.state).toBe('not_home'); }); it('should set the battery level to value', () => { diff --git a/src/integrations/room-presence/room-presence.proxy.ts b/src/integrations/room-presence/room-presence.proxy.ts index 8d1c06c..7f20ebb 100644 --- a/src/integrations/room-presence/room-presence.proxy.ts +++ b/src/integrations/room-presence/room-presence.proxy.ts @@ -1,5 +1,6 @@ import { RoomPresenceDistanceSensor, + STATE_HOME, STATE_NOT_HOME, } from './room-presence-distance.sensor'; import { DeviceTracker } from '../../entities/device-tracker'; @@ -18,7 +19,8 @@ export class RoomPresenceProxyHandler switch (p) { case 'state': - this.deviceTracker.state = value != STATE_NOT_HOME; + this.deviceTracker.state = + value == STATE_NOT_HOME ? STATE_NOT_HOME : STATE_HOME; break; case 'batteryLevel': this.batterySensor.state = value;