UNPKG

matterbridge-hass

Version:
183 lines (182 loc) 16.4 kB
import { colorTemperatureLight, dimmableLight, extendedColorLight } from 'matterbridge'; import { CYAN, db, debugStringify } from 'matterbridge/logger'; import { LevelControl } from 'matterbridge/matter/clusters'; import { getClusterNameById } from 'matterbridge/matter/types'; import { isValidArray, isValidBoolean, isValidNumber, isValidString } from 'matterbridge/utils'; import { getFeatureNames, hassCommandConverter, hassDomainConverter, hassSubscribeConverter, kelvinToMireds, roundTo, temp } from './converters.js'; import { entityHasLabel, getDomain, getEntityName } from './helpers.js'; import { ClimateEntityFeature, ColorMode, DEFAULT_MAX_KELVIN, DEFAULT_MAX_TEMP, DEFAULT_MIN_KELVIN, DEFAULT_MIN_TEMP, FanEntityFeature, HomeAssistant, HVACMode, LightEntityFeature, MediaPlayerEntityFeature, MediaPlayerService, UnitOfTemperature, VacuumEntityFeature, } from './homeAssistant.js'; export function addControlEntity(platform, mutableDevice, entity, state, commandHandler, subscribeHandler) { let endpointName = undefined; const domain = getDomain(entity.entity_id); if (state.state === 'unavailable') { const cachedState = platform.stateCache.get(entity.entity_id); if (cachedState) { platform.log.info(`Entity ${CYAN}${entity.entity_id}${db} is unavailable, using cached state and attributes`); state = cachedState; } else { platform.log.warn(`Entity ${CYAN}${entity.entity_id}${db} is unavailable and no cached state found`); } } hassDomainConverter .filter((d) => d.domain === domain && d.withAttribute === undefined) .forEach((hassDomain) => { if (!hassDomain.deviceType || !hassDomain.clusterId) return; endpointName = entity.entity_id; platform.log.debug(`+ ${domain} device ${CYAN}${hassDomain.deviceType.name}${db} cluster ${CYAN}${getClusterNameById(hassDomain.clusterId)}${db}`); mutableDevice.addDeviceTypes(endpointName, hassDomain.deviceType); mutableDevice.addClusterServerIds(endpointName, hassDomain.clusterId); if (state.attributes && isValidString(state.attributes['friendly_name'])) mutableDevice.setFriendlyName(endpointName, state.attributes['friendly_name']); }); if (endpointName === undefined) return undefined; platform.log.debug(`- state ${debugStringify(state)}`); for (const [key, _value] of Object.entries(state.attributes)) { hassDomainConverter .filter((d) => d.domain === domain && d.withAttribute === key) .forEach((hassDomain) => { if (!hassDomain.deviceType || !hassDomain.clusterId) return; endpointName = entity.entity_id; platform.log.debug(`+ attribute device ${CYAN}${hassDomain.deviceType.name}${db} cluster ${CYAN}${getClusterNameById(hassDomain.clusterId)}${db}`); mutableDevice.addDeviceTypes(endpointName, hassDomain.deviceType); mutableDevice.addClusterServerIds(endpointName, hassDomain.clusterId); }); } if (domain === 'light' && isValidNumber(state.attributes?.supported_features) && isValidArray(state.attributes?.supported_color_modes) && state.attributes.supported_color_modes.includes(ColorMode.BRIGHTNESS)) { platform.log.debug(`+ attribute device ${CYAN}${dimmableLight.name}${db} cluster ${CYAN}${LevelControl.Cluster.name}${db}`); platform.log.debug(`= levelControl device ${CYAN}${entity.entity_id}${db} supported_color_modes: ${CYAN}${state.attributes['supported_color_modes']}${db}`); platform.log.debug(`# levelControl device ${CYAN}${entity.entity_id}${db} supported_features: ${CYAN}${getFeatureNames(LightEntityFeature, state.attributes.supported_features)}${db}`); mutableDevice.addDeviceTypes(endpointName, dimmableLight); mutableDevice.addClusterServerIds(endpointName, LevelControl.Cluster.id); } if (domain === 'light' && (mutableDevice.get(endpointName).deviceTypes.includes(colorTemperatureLight) || mutableDevice.get(endpointName).deviceTypes.includes(extendedColorLight))) { platform.log.debug(`= colorControl device ${CYAN}${entity.entity_id}${db} supported_color_modes: ${CYAN}${state.attributes['supported_color_modes']}${db} min_color_temp_kelvin: ${CYAN}${state.attributes['min_color_temp_kelvin']}${db} max_color_temp_kelvin: ${CYAN}${state.attributes['max_color_temp_kelvin']}${db}`); platform.log.debug(`# colorControl device ${CYAN}${entity.entity_id}${db} supported_features: ${CYAN}${getFeatureNames(LightEntityFeature, state.attributes.supported_features)}${db}`); const minMireds = kelvinToMireds(state.attributes['max_color_temp_kelvin'] ?? DEFAULT_MAX_KELVIN, 'floor'); const maxMireds = kelvinToMireds(state.attributes['min_color_temp_kelvin'] ?? DEFAULT_MIN_KELVIN, 'floor'); platform.log.debug(`= colorControl device ${CYAN}${entity.entity_id}${db} supported_color_modes: ${CYAN}${state.attributes['supported_color_modes']}${db} min_mireds: ${CYAN}${minMireds}${db} max_mireds: ${CYAN}${maxMireds}${db}`); if (isValidArray(state.attributes['supported_color_modes']) && !state.attributes['supported_color_modes'].includes(ColorMode.XY) && !state.attributes['supported_color_modes'].includes(ColorMode.HS) && !state.attributes['supported_color_modes'].includes(ColorMode.RGB) && !state.attributes['supported_color_modes'].includes(ColorMode.RGBW) && !state.attributes['supported_color_modes'].includes(ColorMode.RGBWW) && state.attributes['supported_color_modes'].includes(ColorMode.COLOR_TEMP)) { mutableDevice.addClusterServerColorTemperatureColorControl(endpointName, minMireds, maxMireds); } else { mutableDevice.addClusterServerColorControl(endpointName, minMireds, maxMireds); } } if (domain === 'climate') { const temperature_unit = state.attributes['temperature_unit'] || HomeAssistant.hassConfig?.unit_system?.temperature || UnitOfTemperature.CELSIUS; const current_temperature = isValidNumber(state.attributes['current_temperature']) ? roundTo(temp(state.attributes['current_temperature'], temperature_unit), 2) : null; const min_temp = isValidNumber(state.attributes['min_temp']) ? roundTo(temp(state.attributes['min_temp'], temperature_unit), 2) : DEFAULT_MIN_TEMP; const max_temp = isValidNumber(state.attributes['max_temp']) ? roundTo(temp(state.attributes['max_temp'], temperature_unit), 2) : DEFAULT_MAX_TEMP; const temperature = isValidNumber(state.attributes['temperature']) ? roundTo(temp(state.attributes['temperature'], temperature_unit), 2) : 23; const target_temp_low = isValidNumber(state.attributes['target_temp_low']) ? roundTo(temp(state.attributes['target_temp_low'], temperature_unit), 2) : 20; const target_temp_high = isValidNumber(state.attributes['target_temp_high']) ? roundTo(temp(state.attributes['target_temp_high'], temperature_unit), 2) : 26; platform.log.debug(`= thermostat device ${CYAN}${entity.entity_id}${db} hvac_modes: ${CYAN}${state.attributes['hvac_modes']}${db} temperature_unit: ${CYAN}${temperature_unit}${db} current_temperature: ${CYAN}${current_temperature}${db} min_temp: ${CYAN}${min_temp}${db} max_temp: ${CYAN}${max_temp}${db}`); platform.log.debug(`# thermostat device ${CYAN}${entity.entity_id}${db} supported_features: ${CYAN}${getFeatureNames(ClimateEntityFeature, state.attributes.supported_features)}${db}`); if (!isValidArray(state.attributes['hvac_modes'], 1)) { state.attributes['hvac_modes'] = [HVACMode.HEAT]; platform.log.debug(`Thermostat device ${CYAN}${entity.entity_id}${db} has no hvac_modes attribute, assuming ${CYAN}${HVACMode.HEAT}${db}.`); } if (isValidArray(state.attributes['hvac_modes']) && state.attributes['hvac_modes'].includes(HVACMode.HEAT_COOL)) { platform.log.debug(`= thermostat device ${CYAN}${entity.entity_id}${db} state ${CYAN}${state.attributes['hvac_modes']}${db} auto target_temp_low: ${CYAN}${target_temp_low}${db} target_temp_high: ${CYAN}${target_temp_high}${db}`); mutableDevice.addClusterServerAutoModeThermostat(endpointName, current_temperature, target_temp_low, target_temp_high, min_temp, max_temp); } else if (isValidArray(state.attributes['hvac_modes']) && state.attributes['hvac_modes'].includes(HVACMode.HEAT) && !state.attributes['hvac_modes'].includes(HVACMode.COOL)) { platform.log.debug(`= thermostat device ${CYAN}${entity.entity_id}${db} state ${CYAN}${state.attributes['hvac_modes']}${db} heat temperature: ${CYAN}${temperature}${db}`); mutableDevice.addClusterServerHeatingThermostat(endpointName, current_temperature, temperature, min_temp, max_temp); } else if (isValidArray(state.attributes['hvac_modes']) && state.attributes['hvac_modes'].includes(HVACMode.COOL) && !state.attributes['hvac_modes'].includes(HVACMode.HEAT)) { platform.log.debug(`= thermostat device ${CYAN}${entity.entity_id}${db} state ${CYAN}${state.attributes['hvac_modes']}${db} cool temperature: ${CYAN}${temperature}${db}`); mutableDevice.addClusterServerCoolingThermostat(endpointName, current_temperature, temperature, min_temp, max_temp); } else if (isValidArray(state.attributes['hvac_modes']) && state.attributes['hvac_modes'].includes(HVACMode.COOL) && state.attributes['hvac_modes'].includes(HVACMode.HEAT)) { platform.log.debug(`= thermostat device ${CYAN}${entity.entity_id}${db} state ${CYAN}${state.attributes['hvac_modes']}${db} heat cool temperature: ${CYAN}${temperature}${db}`); mutableDevice.addClusterServerHeatingCoolingThermostat(endpointName, current_temperature, temperature, temperature, min_temp, max_temp); } else { platform.log.debug(`= thermostat device ${CYAN}${entity.entity_id}${db} state ${CYAN}${state.attributes['hvac_modes']}${db} default temperature: ${CYAN}${temperature}${db}`); } } if (domain === 'fan') { platform.log.debug(`= fan device ${CYAN}${entity.entity_id}${db} preset_modes: ${CYAN}${state.attributes['preset_modes']}${db} direction: ${CYAN}${state.attributes['direction']}${db} oscillating: ${CYAN}${state.attributes['oscillating']}${db}`); platform.log.debug(`# fan device ${CYAN}${entity.entity_id}${db} supported_features: ${CYAN}${getFeatureNames(FanEntityFeature, state.attributes.supported_features)}${db}`); if (isValidString(state.attributes['direction']) || isValidBoolean(state.attributes['oscillating'])) { mutableDevice.addClusterServerCompleteFanControl(endpointName); } } if (domain === 'vacuum') { platform.log.debug(`= vacuum device ${CYAN}${entity.entity_id}${db} activity: ${CYAN}${state.attributes['activity']}${db}`); platform.log.debug(`# vacuum device ${CYAN}${entity.entity_id}${db} supported_features: ${CYAN}${getFeatureNames(VacuumEntityFeature, state.attributes.supported_features)}${db}`); mutableDevice.addVacuum(endpointName); } if (domain === 'select' || domain === 'input_select') { platform.log.debug(`= select device ${CYAN}${entity.entity_id}${db} options: ${CYAN}${state.attributes['options']}${db}`); mutableDevice.addSelect(endpointName, getEntityName(platform, entity) ?? 'Select an option', state.attributes['options']); if (entityHasLabel(platform, entity, platform.config.virtualControlLabel)) { state.attributes['options']?.forEach((option) => { platform.log.debug(`***Add select device ${CYAN}${entity.entity_id}${db} virtual control: ${CYAN}${option}${db}`); void platform .registerVirtualDevice(`${getEntityName(platform, entity)} ${option}`, 'mounted_switch', async () => { platform.ha.callService(domain, 'select_option', entity.entity_id, { option }).catch((error) => { platform.log.error(`Failed to call select_option service for ${CYAN}${entity.entity_id}${db} with option ${CYAN}${option}${db}: ${error}`); }); }) .catch(() => { }); }); } } if (domain === 'remote') { platform.log.debug(`= remote device ${CYAN}${entity.entity_id}${db} state: ${CYAN}${state.state}${db}`); mutableDevice.addOnOff(endpointName, true); } if (domain === 'media_player') { platform.log.debug(`= media_player device ${CYAN}${entity.entity_id}${db} state: ${CYAN}${state.state}${db} attrbutes: ${CYAN}${debugStringify(state.attributes)}${db}`); platform.log.debug(`# media_player device ${CYAN}${entity.entity_id}${db} supported_features: ${CYAN}${getFeatureNames(MediaPlayerEntityFeature, state.attributes.supported_features)}${db}`); mutableDevice.addOnOff(endpointName, true); mutableDevice.addBasicVideoPlayer(endpointName); mutableDevice.addKeypadInput(endpointName); if (entityHasLabel(platform, entity, platform.config.virtualControlLabel)) { const featuresServices = [ { feature: MediaPlayerEntityFeature.TURN_ON, service: MediaPlayerService.TURN_ON, controlName: 'Turn ON' }, { feature: MediaPlayerEntityFeature.TURN_OFF, service: MediaPlayerService.TURN_OFF, controlName: 'Turn OFF' }, { feature: MediaPlayerEntityFeature.PLAY, service: MediaPlayerService.MEDIA_PLAY, controlName: 'Play' }, { feature: MediaPlayerEntityFeature.PAUSE, service: MediaPlayerService.MEDIA_PAUSE, controlName: 'Pause' }, { feature: MediaPlayerEntityFeature.STOP, service: MediaPlayerService.MEDIA_STOP, controlName: 'Stop' }, { feature: MediaPlayerEntityFeature.VOLUME_MUTE, service: MediaPlayerService.VOLUME_MUTE, controlName: 'Mute' }, { feature: MediaPlayerEntityFeature.VOLUME_STEP, service: MediaPlayerService.VOLUME_DOWN, controlName: 'Volume Down' }, { feature: MediaPlayerEntityFeature.VOLUME_STEP, service: MediaPlayerService.VOLUME_UP, controlName: 'Volume Up' }, { feature: MediaPlayerEntityFeature.PREVIOUS_TRACK, service: MediaPlayerService.MEDIA_PREVIOUS_TRACK, controlName: 'Previous Track' }, { feature: MediaPlayerEntityFeature.NEXT_TRACK, service: MediaPlayerService.MEDIA_NEXT_TRACK, controlName: 'Next Track' }, ]; featuresServices.forEach(({ feature, service, controlName }) => { if (state.attributes['supported_features'] && state.attributes['supported_features'] & feature) { platform.log.debug(`***Add media_player device ${CYAN}${entity.entity_id}${db} virtual control:${CYAN}${controlName}${db}`); void platform .registerVirtualDevice(`${controlName} ${getEntityName(platform, entity)}`, 'mounted_switch', async () => { platform.ha.callService('media_player', service, entity.entity_id).catch((error) => { platform.log.error(`Failed to call ${controlName.toLowerCase()} service for ${CYAN}${entity.entity_id}${db}: ${error}`); }); }) .catch(() => { }); } }); } } for (const hassCommand of hassCommandConverter.filter((c) => c.domain === domain)) { platform.log.debug(`- command: ${CYAN}${hassCommand.command}${db}`); mutableDevice.addCommandHandler(entity.entity_id, hassCommand.command, (data, endpointName, command) => { void commandHandler(data, endpointName, command).catch(() => { }); }); } for (const hassSubscribe of hassSubscribeConverter.filter((s) => s.domain === domain)) { platform.log.debug(`- subscribe: ${CYAN}${getClusterNameById(hassSubscribe.clusterId)}${db}:${CYAN}${hassSubscribe.attribute}${db}`); mutableDevice.addSubscribeHandler(entity.entity_id, hassSubscribe.clusterId, hassSubscribe.attribute, (newValue, oldValue, context, _endpointName, _clusterId, _attribute) => { subscribeHandler(entity, hassSubscribe, newValue, oldValue, context); }); } return endpointName; }