homebridge-nest-accfactory
Version:
Homebridge support for Nest/Google devices including HomeKit Secure Video (HKSV) support for doorbells and cameras
159 lines (142 loc) • 5.93 kB
JavaScript
// Nest Cam with Floodlight - HomeKit integration
// Part of homebridge-nest-accfactory
//
// HomeKit accessory implementation for Nest Cam with Floodlight.
// Extends NestCamera to add floodlight-specific behaviour and light control
// while reusing camera streaming, snapshots, motion handling, and HKSV support.
//
// Responsibilities:
// - Extend camera functionality with floodlight control
// - Manage HomeKit light service creation and removal
// - Synchronise floodlight on/off and brightness state with HomeKit
// - Route light control changes to the upstream API
//
// Inherits from NestCamera:
// - Video streaming (WebRTC / NexusTalk with HKSV support)
// - Motion detection and smart alerts
// - Snapshot handling and recording (HKSV)
// - Two-way audio when supported
// - Event processing and history integration
//
// Services:
// - CameraController (via NestCamera)
// - Lightbulb (optional, when floodlight support is available)
//
// Features:
// - Combined camera and controllable floodlight accessory
// - Dynamic light service creation based on device capabilities
// - Floodlight on/off control
// - Brightness control in 10% increments
// - Real-time light state synchronisation
//
// Notes:
// - Streaming and recording are handled entirely by NestCamera
// - This module only adds floodlight-specific light control behaviour
// - Camera and floodlight states are managed independently
//
// Mark Hulskamp
'use strict';
// Define external module requirements
import NestCamera, { processRawData as processCameraRawData } from './camera.js';
import { scaleValue } from '../utils.js';
import { DEVICE_TYPE } from '../consts.js';
export default class NestFloodlight extends NestCamera {
static TYPE = DEVICE_TYPE.FLOODLIGHT;
static VERSION = '2026.04.22'; // Code version
lightService = undefined; // HomeKit light
// Class functions
onAdd() {
if (this.deviceData?.has_light === true) {
// Add service for a light, including brightness control
this.lightService = this.addHKService(this.hap.Service.Lightbulb, '', 1);
this.addHKCharacteristic(this.lightService, this.hap.Characteristic.Brightness, {
props: { minStep: 10 }, // Light only goes in 10% increments
onSet: (value) => {
if (value !== this.deviceData.light_brightness) {
this.set({ uuid: this.deviceData.nest_google_device_uuid, light_brightness: value });
this?.log?.info?.('Floodlight brightness on "%s" was set to "%s %"', this.deviceData.description, value);
}
},
onGet: () => {
return this.deviceData.light_brightness;
},
});
this.addHKCharacteristic(this.lightService, this.hap.Characteristic.On, {
onSet: (value) => {
if (value !== this.deviceData.light_enabled) {
this.set({ uuid: this.deviceData.nest_google_device_uuid, light_enabled: value });
this?.log?.info?.('Floodlight on "%s" was turned %s', this.deviceData.description, value === true ? 'on' : 'off');
}
},
onGet: () => {
return this.deviceData.light_enabled === true;
},
});
// Extra setup details for output
this.lightService !== undefined && this.postSetupDetail('Light support');
}
if (this.deviceData?.has_light !== true) {
// No longer required to have the light service
this.lightService = this.accessory.getService(this.hap.Service.Lightbulb);
if (this.lightService !== undefined) {
this.accessory.removeService(this.lightService);
}
this.lightService = undefined;
}
}
onRemove() {
if (this.lightService !== undefined) {
this.accessory.removeService(this.lightService);
}
this.lightService = undefined;
}
onUpdate(deviceData) {
if (typeof deviceData !== 'object' || this.controller === undefined) {
return;
}
if (this.lightService !== undefined) {
// Update status of light, including brightness
this.lightService.updateCharacteristic(this.hap.Characteristic.On, deviceData.light_enabled);
this.lightService.updateCharacteristic(this.hap.Characteristic.Brightness, deviceData.light_brightness);
// Log floodlight on/off state changes
if (typeof deviceData?.light_enabled === 'boolean' && deviceData.light_enabled !== this.deviceData.light_enabled) {
this?.log?.info?.('Floodlight light on "%s" turned %s', deviceData.description, deviceData.light_enabled === true ? 'on' : 'off');
}
}
}
}
// Floodlight extra field translation map
// Maps raw source data -> normalised floodlight-specific fields
// - fields: top-level raw fields this mapping depends on (for delta updates)
// - related: top-level raw fields on related objects this mapping depends on
// - translate: converts raw -> final normalised value
// - required: extends base camera required fields
const EXTRA_FIELD_MAP = {
has_light: {
required: true,
google: {
fields: ['floodlight_settings', 'floodlight_state'],
translate: ({ raw }) => typeof raw?.value?.floodlight_settings === 'object' && typeof raw?.value?.floodlight_state === 'object',
},
},
light_enabled: {
required: true,
google: {
fields: ['floodlight_state'],
translate: ({ raw }) => raw?.value?.floodlight_state?.currentState === 'LIGHT_STATE_ON',
},
},
light_brightness: {
required: true,
google: {
fields: ['floodlight_settings'],
translate: ({ raw }) =>
Number.isFinite(Number(raw?.value?.floodlight_settings?.brightness)) === true
? scaleValue(Number(raw.value.floodlight_settings.brightness), 0, 10, 0, 100)
: undefined,
},
},
};
export function processRawData(log, rawData, config, deviceType = undefined, changedData = undefined) {
return processCameraRawData(log, rawData, config, deviceType, changedData, EXTRA_FIELD_MAP);
}