matterbridge-hass
Version:
Matterbridge hass plugin
445 lines (444 loc) • 31.9 kB
JavaScript
import { bridgedNode, colorTemperatureLight, MatterbridgeColorControlServer, MatterbridgeDynamicPlatform, MatterbridgeThermostatServer, onOffOutlet, } from 'matterbridge';
import { dn, idn, ign, nf, rs, wr, db, or, debugStringify, YELLOW, CYAN, hk } from 'matterbridge/logger';
import { deepEqual, isValidArray, isValidString, waiter } from 'matterbridge/utils';
import { NodeStorageManager } from 'matterbridge/storage';
import { Thermostat, OnOff, ColorControl } from 'matterbridge/matter/clusters';
import { ClusterRegistry } from 'matterbridge/matter/types';
import path from 'node:path';
import { promises as fs } from 'node:fs';
import { HomeAssistant } from './homeAssistant.js';
import { MutableDevice, getClusterServerObj } from './mutableDevice.js';
import { hassCommandConverter, hassDomainAttributeConverter, hassDomainConverter, hassDomainSensorsConverter, hassSubscribeConverter, hassUpdateAttributeConverter, hassUpdateStateConverter, } from './converters.js';
export class HomeAssistantPlatform extends MatterbridgeDynamicPlatform {
nodeStorageManager;
nodeStorage;
ha;
matterbridgeDevices = new Map();
bridgedHassDevices = new Map();
bridgedHassEntities = new Map();
constructor(matterbridge, log, config) {
super(matterbridge, log, config);
if (this.verifyMatterbridgeVersion === undefined || typeof this.verifyMatterbridgeVersion !== 'function' || !this.verifyMatterbridgeVersion('2.2.6')) {
throw new Error(`This plugin requires Matterbridge version >= "2.2.6". Please update Matterbridge from ${this.matterbridge.matterbridgeVersion} to the latest version in the frontend."`);
}
this.log.info(`Initializing platform: ${CYAN}${this.config.name}${nf} version: ${CYAN}${this.config.version}${rs}`);
if (!isValidString(config.host, 1) || !isValidString(config.token, 1)) {
throw new Error('Host and token must be defined in the configuration');
}
this.ha = new HomeAssistant(config.host, config.token, config.reconnectTimeout ?? 60);
this.ha.on('connected', (ha_version) => {
this.log.notice(`Connected to Home Assistant ${ha_version}`);
});
this.ha.on('disconnected', () => {
this.log.warn('Disconnected from Home Assistant');
});
this.ha.on('subscribed', () => {
this.log.info(`Subscribed to Home Assistant events`);
});
this.ha.on('config', (config) => {
this.log.info('Configuration received from Home Assistant');
});
this.ha.on('services', (services) => {
this.log.info('Services received from Home Assistant');
});
this.ha.on('devices', (devices) => {
this.log.info('Devices received from Home Assistant');
});
this.ha.on('entities', (entities) => {
this.log.info('Entities received from Home Assistant');
});
this.ha.on('states', (states) => {
this.log.info('States received from Home Assistant');
});
this.ha.on('event', this.updateHandler.bind(this));
}
async onStart(reason) {
this.log.info(`Starting platform ${idn}${this.config.name}${rs}${nf}: ${reason ?? ''}`);
this.nodeStorageManager = new NodeStorageManager({
dir: path.join(this.matterbridge.matterbridgeDirectory, 'matterbridge-hass'),
writeQueue: false,
expiredInterval: undefined,
logging: false,
forgiveParseErrors: true,
});
this.nodeStorage = await this.nodeStorageManager.createStorage('devices');
await fs.mkdir(path.join(this.matterbridge.matterbridgePluginDirectory, 'matterbridge-hass'), { recursive: true });
this.ha.connect();
const check = () => {
return this.ha.connected && this.ha.devicesReceived && this.ha.entitiesReceived && this.ha.subscribed;
};
await waiter('Home Assistant connected', check, true, 10000, 1000);
const payload = {
devices: Array.from(this.ha.hassDevices.values()),
entities: Array.from(this.ha.hassEntities.values()),
states: Array.from(this.ha.hassStates.values()),
config: this.ha.hassConfig,
services: this.ha.hassServices,
};
fs.writeFile(path.join(this.matterbridge.matterbridgePluginDirectory, 'matterbridge-hass', 'homeassistant.json'), JSON.stringify(payload, null, 2))
.then(() => {
this.log.debug('Payload successfully written to homeassistant.json');
})
.catch((error) => {
this.log.error('Error writing payload to file:', error);
});
await this.clearSelect();
for (const entity of Array.from(this.ha.hassEntities.values())) {
const [domain, name] = entity.entity_id.split('.');
if (!['automation', 'scene', 'script', 'input_boolean'].includes(domain))
continue;
const entityName = entity.name ?? entity.original_name;
if (!isValidString(entityName))
continue;
this.setSelectEntity(entity.entity_id, entityName, 'hub');
if (isValidArray(this.config.individualEntityWhiteList, 1) &&
!this.config.individualEntityWhiteList.includes(entityName) &&
!this.config.individualEntityWhiteList.includes(entity.entity_id))
continue;
if (isValidArray(this.config.individualEntityBlackList, 1) &&
(this.config.individualEntityBlackList.includes(entityName) || this.config.individualEntityBlackList.includes(entity.entity_id)))
continue;
if (this.hasDeviceName(entityName)) {
this.log.warn(`Entity ${CYAN}${entityName}${nf} already exists as a registered device. Please change the name in Home Assistant`);
continue;
}
this.log.info(`Creating device for individual entity ${idn}${entityName}${rs}${nf} domain ${CYAN}${domain}${nf} name ${CYAN}${name}${nf}`);
const mutableDevice = new MutableDevice(this.matterbridge, entityName + (isValidString(this.config.namePostfix, 1, 3) ? ' ' + this.config.namePostfix : ''), isValidString(this.config.serialPostfix, 1, 3) ? entity.id.slice(0, 29) + this.config.serialPostfix : entity.id, 0xfff1, 'HomeAssistant', domain);
mutableDevice.addDeviceTypes('', bridgedNode);
mutableDevice.composedType = 'HomeAssistant';
const matterbridgeDevice = await mutableDevice.createMainEndpoint();
if (domain === 'automation')
matterbridgeDevice.configUrl = `${this.config.host?.replace('ws://', 'http://')}/config/automation/dashboard`;
else if (domain === 'scene')
matterbridgeDevice.configUrl = `${this.config.host?.replace('ws://', 'http://')}/config/scene/dashboard`;
else if (domain === 'script')
matterbridgeDevice.configUrl = `${this.config.host?.replace('ws://', 'http://')}/config/script/dashboard`;
else if (domain === 'input_boolean')
matterbridgeDevice.configUrl = `${this.config.host?.replace('ws://', 'http://')}/config/helpers`;
await mutableDevice.createClusters('');
mutableDevice.addDeviceTypes(entity.entity_id, onOffOutlet);
mutableDevice.setFriendlyName(entity.entity_id, entityName);
const child = await mutableDevice.createChildEndpoint(entity.entity_id);
await mutableDevice.createClusters(entity.entity_id);
child.addCommandHandler('on', async () => {
await this.ha.callServiceAsync(domain, domain === 'automation' ? 'trigger' : 'turn_on', entity.entity_id);
if (domain !== 'input_boolean') {
setTimeout(() => {
child.setAttribute(OnOff.Cluster.id, 'onOff', false, child.log);
}, 500);
}
});
child.addCommandHandler('off', async () => {
if (domain === 'input_boolean')
await this.ha.callServiceAsync(domain, 'turn_off', entity.entity_id);
});
this.log.debug(`Registering device ${dn}${entityName}${db}...`);
mutableDevice.logMutableDevice();
await this.registerDevice(mutableDevice.getEndpoint());
this.matterbridgeDevices.set(entity.entity_id, mutableDevice.getEndpoint());
this.bridgedHassEntities.set(entity.entity_id, entity);
}
for (const device of Array.from(this.ha.hassDevices.values())) {
const deviceName = device.name_by_user ?? device.name;
const entitiesCount = Array.from(this.ha.hassEntities.values()).filter((e) => e.device_id === device.id).length;
if (deviceName && entitiesCount > 0)
this.setSelectDevice(device.id, deviceName, undefined, 'hub');
if (!isValidString(deviceName, 1) || !this.validateDevice([deviceName, device.id], true))
continue;
if (this.hasDeviceName(deviceName)) {
this.log.warn(`Device ${CYAN}${deviceName}${nf} already exists as a registered device. Please change the name in Home Assistant`);
continue;
}
this.log.info(`Creating device ${idn}${device.name}${rs}${nf} id ${CYAN}${device.id}${nf}`);
const mutableDevice = new MutableDevice(this.matterbridge, deviceName + (isValidString(this.config.namePostfix, 1, 3) ? ' ' + this.config.namePostfix : ''), isValidString(this.config.serialPostfix, 1, 3) ? device.id.slice(0, 29) + this.config.serialPostfix : device.id, 0xfff1, 'HomeAssistant', device.model ?? 'Unknown');
mutableDevice.addDeviceTypes('', bridgedNode);
mutableDevice.composedType = 'HomeAssistant';
const matterbridgeDevice = await mutableDevice.createMainEndpoint();
matterbridgeDevice.configUrl = `${this.config.host?.replace('ws://', 'http://')}/config/devices/device/${device.id}`;
for (const entity of Array.from(this.ha.hassEntities.values()).filter((e) => e.device_id === device.id)) {
this.log.debug(`Lookup device ${CYAN}${device.name}${db} entity ${CYAN}${entity.entity_id}${db}`);
if (!this.validateEntity(deviceName, entity.entity_id, true))
continue;
const domain = entity.entity_id.split('.')[0];
const hassState = this.ha.hassStates.get(entity.entity_id);
if (!hassState) {
this.log.debug(`Lookup device ${CYAN}${device.name}${db} entity ${CYAN}${entity.entity_id}${db}: state not found`);
continue;
}
const hassDomains = hassDomainConverter.filter((d) => d.domain === domain);
if (hassDomains.length > 0) {
this.log.debug(`Lookup device ${CYAN}${device.name}${db} domain ${CYAN}${CYAN}${domain}${db} entity ${CYAN}${entity.entity_id}${db}`);
this.setSelectDeviceEntity(device.id, entity.entity_id, entity.name ?? entity.original_name ?? '', 'component');
hassDomains.forEach((hassDomain) => {
if (hassDomain.deviceType)
mutableDevice.addDeviceTypes(entity.entity_id, hassDomain.deviceType);
if (hassDomain.clusterId)
mutableDevice.addClusterServerIds(entity.entity_id, hassDomain.clusterId);
if (hassDomain.deviceType && isValidString(hassState.attributes['friendly_name']))
mutableDevice.setFriendlyName(entity.entity_id, hassState.attributes['friendly_name']);
});
}
else {
this.log.debug(`Lookup device ${CYAN}${device.name}${db} domain ${CYAN}${CYAN}${domain}${db} entity ${CYAN}${entity.entity_id}${db}: domain not found`);
continue;
}
this.log.debug(`- state ${debugStringify(hassState)}`);
for (const [key, value] of Object.entries(hassState.attributes)) {
this.log.debug(`- attribute ${CYAN}${key}${db} value ${typeof value === 'object' && value ? debugStringify(value) : value}`);
const hassDomainAttributes = hassDomainAttributeConverter.filter((d) => d.domain === domain && d.with === key);
hassDomainAttributes.forEach((hassDomainAttribute) => {
this.log.debug(`+ attribute device ${CYAN}${hassDomainAttribute.deviceType.name}${db} cluster ${CYAN}${ClusterRegistry.get(hassDomainAttribute.clusterId)?.name}${db}`);
mutableDevice.addDeviceTypes(entity.entity_id, hassDomainAttribute.deviceType);
mutableDevice.addClusterServerIds(entity.entity_id, hassDomainAttribute.clusterId);
});
}
const hassDomainSensors = hassDomainSensorsConverter.filter((d) => d.domain === domain);
hassDomainSensors.forEach((hassDomainSensor) => {
this.log.debug(`- sensor ${CYAN}${hassDomainSensor.domain}${db} stateClass ${hassDomainSensor.withStateClass} deviceClass ${hassDomainSensor.withDeviceClass}`);
if (hassState.attributes['state_class'] === hassDomainSensor.withStateClass && hassState.attributes['device_class'] === hassDomainSensor.withDeviceClass) {
this.log.debug(`+ sensor device ${CYAN}${hassDomainSensor.deviceType.name}${db} cluster ${CYAN}${ClusterRegistry.get(hassDomainSensor.clusterId)?.name}${db}`);
mutableDevice.addDeviceTypes(entity.entity_id, hassDomainSensor.deviceType);
mutableDevice.addClusterServerIds(entity.entity_id, hassDomainSensor.clusterId);
if (isValidString(hassState.attributes['friendly_name']))
mutableDevice.setFriendlyName(entity.entity_id, hassState.attributes['friendly_name']);
}
});
if (!mutableDevice.has(entity.entity_id))
continue;
this.log.info(`Creating endpoint ${CYAN}${entity.entity_id}${nf} for device ${idn}${device.name}${rs}${nf} id ${CYAN}${device.id}${nf}`);
const child = await mutableDevice.createChildEndpoint(entity.entity_id);
if (domain === 'light' && mutableDevice.get(entity.entity_id).deviceTypes[0] === colorTemperatureLight) {
if (isValidArray(hassState.attributes['supported_color_modes']) &&
!hassState.attributes['supported_color_modes'].includes('xy') &&
!hassState.attributes['supported_color_modes'].includes('hs') &&
!hassState.attributes['supported_color_modes'].includes('rgb') &&
hassState.attributes['supported_color_modes'].includes('color_temp')) {
mutableDevice.addClusterServerObjs(entity.entity_id, getClusterServerObj(ColorControl.Cluster.id, MatterbridgeColorControlServer.with(ColorControl.Feature.ColorTemperature), {
colorMode: ColorControl.ColorMode.ColorTemperatureMireds,
enhancedColorMode: ColorControl.EnhancedColorMode.ColorTemperatureMireds,
colorCapabilities: { xy: false, hueSaturation: false, colorLoop: false, enhancedHue: false, colorTemperature: true },
options: {
executeIfOff: false,
},
numberOfPrimaries: null,
colorTemperatureMireds: hassState.attributes['max_mireds'],
colorTempPhysicalMinMireds: hassState.attributes['min_mireds'],
colorTempPhysicalMaxMireds: hassState.attributes['max_mireds'],
coupleColorTempToLevelMinMireds: hassState.attributes['min_mireds'],
remainingTime: 0,
startUpColorTemperatureMireds: null,
}));
}
else {
mutableDevice.addClusterServerObjs(entity.entity_id, getClusterServerObj(ColorControl.Cluster.id, MatterbridgeColorControlServer.with(ColorControl.Feature.ColorTemperature, ColorControl.Feature.HueSaturation, ColorControl.Feature.Xy), {
colorMode: ColorControl.ColorMode.CurrentHueAndCurrentSaturation,
enhancedColorMode: ColorControl.EnhancedColorMode.CurrentHueAndCurrentSaturation,
colorCapabilities: { xy: true, hueSaturation: true, colorLoop: false, enhancedHue: false, colorTemperature: true },
options: {
executeIfOff: false,
},
numberOfPrimaries: null,
colorTemperatureMireds: hassState.attributes['max_mireds'],
colorTempPhysicalMinMireds: hassState.attributes['min_mireds'],
colorTempPhysicalMaxMireds: hassState.attributes['max_mireds'],
coupleColorTempToLevelMinMireds: hassState.attributes['min_mireds'],
remainingTime: 0,
startUpColorTemperatureMireds: null,
}));
}
}
if (domain === 'climate') {
if (isValidArray(hassState?.attributes['hvac_modes']) && hassState.attributes['hvac_modes'].includes('heat_cool')) {
mutableDevice.addClusterServerObjs(entity.entity_id, getClusterServerObj(Thermostat.Cluster.id, MatterbridgeThermostatServer.with(Thermostat.Feature.AutoMode, Thermostat.Feature.Heating, Thermostat.Feature.Cooling), {
localTemperature: (hassState.attributes['current_temperature'] ?? 23) * 100,
systemMode: Thermostat.SystemMode.Auto,
controlSequenceOfOperation: Thermostat.ControlSequenceOfOperation.CoolingAndHeating,
occupiedHeatingSetpoint: (hassState.attributes['target_temp_low'] ?? 21) * 100,
minHeatSetpointLimit: (hassState.attributes['min_temp'] ?? 0) * 100,
absMinHeatSetpointLimit: (hassState.attributes['min_temp'] ?? 0) * 100,
maxHeatSetpointLimit: (hassState.attributes['max_temp'] ?? 50) * 100,
absMaxHeatSetpointLimit: (hassState.attributes['max_temp'] ?? 50) * 100,
occupiedCoolingSetpoint: (hassState.attributes['target_temp_high'] ?? 25) * 100,
minCoolSetpointLimit: (hassState.attributes['min_temp'] ?? 0) * 100,
absMinCoolSetpointLimit: (hassState.attributes['min_temp'] ?? 0) * 100,
maxCoolSetpointLimit: (hassState.attributes['max_temp'] ?? 50) * 100,
absMaxCoolSetpointLimit: (hassState.attributes['max_temp'] ?? 50) * 100,
minSetpointDeadBand: 1 * 100,
thermostatRunningMode: Thermostat.ThermostatRunningMode.Off,
}));
}
else if (isValidArray(hassState?.attributes['hvac_modes']) &&
hassState.attributes['hvac_modes'].includes('heat') &&
!hassState.attributes['hvac_modes'].includes('cool')) {
mutableDevice.addClusterServerObjs(entity.entity_id, getClusterServerObj(Thermostat.Cluster.id, MatterbridgeThermostatServer.with(Thermostat.Feature.Heating), {
localTemperature: (hassState.attributes['current_temperature'] ?? 23) * 100,
systemMode: Thermostat.SystemMode.Heat,
controlSequenceOfOperation: Thermostat.ControlSequenceOfOperation.HeatingOnly,
occupiedHeatingSetpoint: (hassState.attributes['temperature'] ?? 21) * 100,
minHeatSetpointLimit: (hassState.attributes['min_temp'] ?? 0) * 100,
absMinHeatSetpointLimit: (hassState.attributes['min_temp'] ?? 0) * 100,
maxHeatSetpointLimit: (hassState.attributes['max_temp'] ?? 50) * 100,
absMaxHeatSetpointLimit: (hassState.attributes['max_temp'] ?? 50) * 100,
}));
}
else if (isValidArray(hassState?.attributes['hvac_modes']) &&
hassState.attributes['hvac_modes'].includes('cool') &&
!hassState.attributes['hvac_modes'].includes('heat')) {
mutableDevice.addClusterServerObjs(entity.entity_id, getClusterServerObj(Thermostat.Cluster.id, MatterbridgeThermostatServer.with(Thermostat.Feature.Cooling), {
localTemperature: (hassState.attributes['current_temperature'] ?? 23) * 100,
systemMode: Thermostat.SystemMode.Cool,
controlSequenceOfOperation: Thermostat.ControlSequenceOfOperation.CoolingOnly,
occupiedCoolingSetpoint: (hassState.attributes['temperature'] ?? 25) * 100,
minCoolSetpointLimit: (hassState.attributes['min_temp'] ?? 0) * 100,
absMinCoolSetpointLimit: (hassState.attributes['min_temp'] ?? 0) * 100,
maxCoolSetpointLimit: (hassState.attributes['max_temp'] ?? 50) * 100,
absMaxCoolSetpointLimit: (hassState.attributes['max_temp'] ?? 50) * 100,
}));
}
}
await mutableDevice.createClusters(entity.entity_id);
const hassCommands = hassCommandConverter.filter((c) => c.domain === domain);
if (hassCommands.length > 0) {
hassCommands.forEach((hassCommand) => {
this.log.debug(`- command: ${CYAN}${hassCommand.command}${db}`);
child.addCommandHandler(hassCommand.command, async (data) => {
this.commandHandler(matterbridgeDevice, data.endpoint, data.request, data.attributes, hassCommand.command);
});
});
}
const hassSubscribed = hassSubscribeConverter.filter((s) => s.domain === domain);
if (hassSubscribed.length > 0) {
for (const hassSubscribe of hassSubscribed) {
const check = child.hasAttributeServer(hassSubscribe.clusterId, hassSubscribe.attribute);
this.log.debug(`- subscribe: ${CYAN}${ClusterRegistry.get(hassSubscribe.clusterId)?.name}${db}:${CYAN}${hassSubscribe.attribute}${db} check ${CYAN}${check}${db}`);
if (!check)
continue;
child.subscribeAttribute(hassSubscribe.clusterId, hassSubscribe.attribute, (newValue, oldValue) => {
if ((typeof newValue !== 'object' && newValue === oldValue) || (typeof newValue === 'object' && deepEqual(newValue, oldValue))) {
matterbridgeDevice?.log.debug(`Subscribed attribute ${hk}${ClusterRegistry.get(hassSubscribe.clusterId)?.name}${db}:${hk}${hassSubscribe.attribute}${db} ` +
`on endpoint ${or}${child?.name}${db}:${or}${child?.number}${db} not changed`);
return;
}
matterbridgeDevice?.log.info(`${db}Subscribed attribute ${hk}${ClusterRegistry.get(hassSubscribe.clusterId)?.name}${db}:${hk}${hassSubscribe.attribute}${db} on endpoint ${or}${child?.name}${db}:${or}${child?.number}${db} ` +
`changed from ${YELLOW}${typeof oldValue === 'object' ? debugStringify(oldValue) : oldValue}${db} to ${YELLOW}${typeof newValue === 'object' ? debugStringify(newValue) : newValue}${db}`);
const value = hassSubscribe.converter ? hassSubscribe.converter(newValue) : newValue;
matterbridgeDevice?.log.debug(`Converter(${hassSubscribe.converter !== undefined}): ${typeof newValue === 'object' ? debugStringify(newValue) : newValue} => ${typeof value === 'object' ? debugStringify(value) : value}`);
if (value !== null)
this.ha.callServiceAsync(domain, hassSubscribe.service, entity.entity_id, { [hassSubscribe.with]: value });
else
this.ha.callServiceAsync(domain, 'turn_off', entity.entity_id);
}, child.log);
}
}
}
await mutableDevice.createClusters('');
if (matterbridgeDevice && matterbridgeDevice.getChildEndpoints().length > 0) {
this.log.debug(`Registering device ${dn}${device.name}${db}...`);
mutableDevice.logMutableDevice();
await this.registerDevice(mutableDevice.getEndpoint());
this.matterbridgeDevices.set(device.id, mutableDevice.getEndpoint());
this.bridgedHassDevices.set(device.id, device);
}
}
}
async onConfigure() {
await super.onConfigure();
this.log.info(`Configuring platform ${idn}${this.config.name}${rs}${nf}`);
try {
for (const state of Array.from(this.ha.hassStates.values())) {
const entity = this.ha.hassEntities.get(state.entity_id);
const deviceId = entity?.device_id;
if (deviceId && this.bridgedHassDevices.has(deviceId)) {
this.log.debug(`Configuring state ${CYAN}${state.entity_id}${db} for device ${CYAN}${deviceId}${db}`);
this.updateHandler(deviceId, state.entity_id, state, state);
}
}
}
catch (error) {
this.log.error(`Error configuring platform: ${error}`);
}
}
async onChangeLoggerLevel(logLevel) {
this.log.info(`Logger level changed to ${logLevel}`);
for (const device of this.matterbridgeDevices.values()) {
device.log.logLevel = logLevel;
}
}
async onShutdown(reason) {
await super.onShutdown(reason);
this.log.info(`Shutting down platform ${idn}${this.config.name}${rs}${nf}: ${reason ?? ''}`);
this.ha.close();
this.ha.removeAllListeners();
if (this.config.unregisterOnShutdown === true)
await this.unregisterAllDevices();
this.matterbridgeDevices.clear();
this.bridgedHassDevices.clear();
this.bridgedHassEntities.clear();
}
async commandHandler(mbDevice, endpoint, request, attributes, command) {
if (!mbDevice) {
this.log.error(`Command handler: Matterbridge device not found`);
return;
}
mbDevice.log.info(`${db}Received matter command ${ign}${command}${rs}${db} from device ${idn}${mbDevice?.deviceName}${rs}${db} for endpoint ${or}${endpoint.name}:${endpoint.number}${db}`);
const entityId = endpoint.number ? mbDevice.getChildEndpoint(endpoint.number)?.uniqueStorageKey : undefined;
if (!entityId)
return;
const domain = entityId.split('.')[0];
const hassCommand = hassCommandConverter.find((cvt) => cvt.command === command && cvt.domain === domain);
if (hassCommand) {
const serviceAttributes = hassCommand.converter ? hassCommand.converter(request, attributes) : undefined;
await this.ha.callServiceAsync(hassCommand.domain, hassCommand.service, entityId, serviceAttributes);
}
else {
mbDevice.log.warn(`Command ${ign}${command}${rs}${wr} not supported for domain ${CYAN}${domain}${wr} entity ${CYAN}${entityId}${wr}`);
}
}
async updateHandler(deviceId, entityId, old_state, new_state) {
const matterbridgeDevice = this.matterbridgeDevices.get(deviceId ?? entityId);
if (!matterbridgeDevice)
return;
const endpoint = matterbridgeDevice.getChildEndpointByName(entityId) || matterbridgeDevice.getChildEndpointByName(entityId.replaceAll('.', ''));
if (!endpoint)
return;
matterbridgeDevice.log.info(`${db}Received update event from Home Assistant device ${idn}${matterbridgeDevice?.deviceName}${rs}${db} entity ${CYAN}${entityId}${db} ` +
`from ${YELLOW}${old_state.state}${db} with ${debugStringify(old_state.attributes)}${db} to ${YELLOW}${new_state.state}${db} with ${debugStringify(new_state.attributes)}`);
const domain = entityId.split('.')[0];
if (['automation', 'scene', 'script'].includes(domain)) {
return;
}
else if (domain === 'sensor') {
const hassSensorConverter = hassDomainSensorsConverter.find((s) => s.domain === domain && s.withStateClass === new_state.attributes['state_class'] && s.withDeviceClass === new_state.attributes['device_class']);
if (hassSensorConverter) {
const convertedValue = hassSensorConverter.converter(parseFloat(new_state.state));
endpoint.log.debug(`Converting sensor ${new_state.attributes['state_class']}:${new_state.attributes['device_class']} value "${new_state.state}" to ${CYAN}${convertedValue}${db}`);
if (convertedValue !== null)
await endpoint.setAttribute(hassSensorConverter.clusterId, hassSensorConverter.attribute, convertedValue, endpoint.log);
}
else {
endpoint.log.warn(`Update sensor ${CYAN}${domain}${wr}:${CYAN}${new_state.attributes['state_class']}${wr}:${CYAN}${new_state.attributes['device_class']}${wr} not supported for entity ${entityId}`);
}
}
else {
const hassUpdateState = hassUpdateStateConverter.find((updateState) => updateState.domain === domain && updateState.state === new_state.state);
if (hassUpdateState) {
await endpoint.setAttribute(hassUpdateState.clusterId, hassUpdateState.attribute, hassUpdateState.value, matterbridgeDevice.log);
}
else {
endpoint.log.warn(`Update state ${CYAN}${domain}${wr}:${CYAN}${new_state.state}${wr} not supported for entity ${entityId}`);
}
const hassUpdateAttributes = hassUpdateAttributeConverter.filter((updateAttribute) => updateAttribute.domain === domain);
if (hassUpdateAttributes.length > 0) {
for (const update of hassUpdateAttributes) {
const value = new_state.attributes[update.with];
if (value !== null) {
const convertedValue = update.converter(value, new_state);
endpoint.log.debug(`Converting attribute ${update.with} value ${value} to ${CYAN}${convertedValue}${db}`);
if (convertedValue !== null)
await endpoint.setAttribute(update.clusterId, update.attribute, convertedValue, endpoint.log);
}
}
}
}
}
}