Skip to content

Commit

Permalink
feat(home-assistant): add device tracker discovery (#425)
Browse files Browse the repository at this point in the history
  • Loading branch information
PeteBa committed Jan 31, 2021
1 parent 3fa388d commit 5e3a727
Show file tree
Hide file tree
Showing 10 changed files with 89 additions and 46 deletions.
14 changes: 0 additions & 14 deletions docs/integrations/home-assistant.md
Expand Up @@ -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 |
Expand Down
2 changes: 1 addition & 1 deletion src/entities/device-tracker.ts
@@ -1,5 +1,5 @@
import { Entity } from './entity.dto';

export class DeviceTracker extends Entity {
state: boolean;
state: string;
}
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand All @@ -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])
Expand Down Expand Up @@ -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),
Expand Down
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand All @@ -259,7 +260,8 @@ export class BluetoothLowEnergyService

const deviceTracker = this.createDeviceTracker(
makeId(`${sensorId}-tracker`),
`${deviceName} Tracker`
`${deviceName} Tracker`,
deviceInfo
);

let batterySensor: Sensor;
Expand Down Expand Up @@ -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<EntityCustomization<any>> = [
{
for: DeviceTrackerConfig,
overrides: {
device: deviceInfo,
},
},
];
return this.entitiesService.add(
new DeviceTracker(id, name, true)
new DeviceTracker(id, name, true),
trackerCustomizations
) as DeviceTracker;
}

Expand Down
10 changes: 8 additions & 2 deletions 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);
Expand Down
29 changes: 27 additions & 2 deletions src/integrations/home-assistant/home-assistant.service.spec.ts
Expand Up @@ -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 () => {
Expand Down
29 changes: 11 additions & 18 deletions src/integrations/home-assistant/home-assistant.service.ts
Expand Up @@ -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,
Expand Down
@@ -1,5 +1,6 @@
import { Sensor } from '../../entities/sensor';

export const STATE_HOME = 'home';
export const STATE_NOT_HOME = 'not_home';

class TimedDistance {
Expand Down
2 changes: 1 addition & 1 deletion src/integrations/room-presence/room-presence.proxy.spec.ts
Expand Up @@ -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', () => {
Expand Down
4 changes: 3 additions & 1 deletion 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';
Expand All @@ -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;
Expand Down

0 comments on commit 5e3a727

Please sign in to comment.