794 lines
38 KiB
TypeScript
794 lines
38 KiB
TypeScript
|
|
import * as plugins from '../../plugins.js';
|
||
|
|
import type { IIntegrationEntity, IServiceCallRequest } from '../../core/types.js';
|
||
|
|
import type {
|
||
|
|
IBoschShcBinarySensorFeature,
|
||
|
|
IBoschShcClimateFeature,
|
||
|
|
IBoschShcCoverFeature,
|
||
|
|
IBoschShcDevice,
|
||
|
|
IBoschShcDeviceService,
|
||
|
|
IBoschShcFeatureBase,
|
||
|
|
IBoschShcLightFeature,
|
||
|
|
IBoschShcModeledCommand,
|
||
|
|
IBoschShcSensorFeature,
|
||
|
|
IBoschShcSnapshot,
|
||
|
|
IBoschShcSwitchFeature,
|
||
|
|
TBoschShcCoverState,
|
||
|
|
TBoschShcFeature,
|
||
|
|
} from './bosch_shc.types.js';
|
||
|
|
|
||
|
|
const thermostatModels = new Set(['TRV', 'TRV_GEN2', 'TRV_GEN2_DUAL']);
|
||
|
|
const wallThermostatModels = new Set(['THB', 'BWTH', 'BWTH24', 'RTH2_BAT', 'RTH2_230']);
|
||
|
|
const shutterContactModels = new Set(['SWD', 'SWD2', 'SWD2_PLUS', 'SWD2_DUAL']);
|
||
|
|
const batteryModels = new Set([
|
||
|
|
'MD',
|
||
|
|
'SWD',
|
||
|
|
'SWD2',
|
||
|
|
'SWD2_PLUS',
|
||
|
|
'SWD2_DUAL',
|
||
|
|
'SD',
|
||
|
|
'SMOKE_DETECTOR2',
|
||
|
|
'TRV',
|
||
|
|
'TRV_GEN2',
|
||
|
|
'TRV_GEN2_DUAL',
|
||
|
|
'TWINGUARD',
|
||
|
|
'WRC2',
|
||
|
|
'SWITCH2',
|
||
|
|
'THB',
|
||
|
|
'BWTH',
|
||
|
|
'BWTH24',
|
||
|
|
'RTH2_BAT',
|
||
|
|
'RTH2_230',
|
||
|
|
'WLS',
|
||
|
|
]);
|
||
|
|
const smartPlugModels = new Set(['PSM', 'PLUG_COMPACT', 'PLUG_COMPACT_DUAL']);
|
||
|
|
const lightSwitchModels = new Set(['BSM', 'MICROMODULE_LIGHT_ATTACHED', 'MICROMODULE_RELAY']);
|
||
|
|
const lightModels = new Set(['LEDVANCE_LIGHT', 'HUE_LIGHT', 'MICROMODULE_DIMMER']);
|
||
|
|
|
||
|
|
const sensorMetadata: Record<string, { name: string; unit?: string; deviceClass?: string; stateClass?: string }> = {
|
||
|
|
temperature: { name: 'Temperature', unit: 'C', deviceClass: 'temperature', stateClass: 'measurement' },
|
||
|
|
humidity: { name: 'Humidity', unit: '%', deviceClass: 'humidity' },
|
||
|
|
valvetappet: { name: 'Valvetappet', unit: '%', stateClass: 'measurement' },
|
||
|
|
purity: { name: 'Purity', unit: 'ppm' },
|
||
|
|
airquality: { name: 'Air quality' },
|
||
|
|
temperature_rating: { name: 'Temperature rating' },
|
||
|
|
humidity_rating: { name: 'Humidity rating' },
|
||
|
|
purity_rating: { name: 'Purity rating' },
|
||
|
|
power: { name: 'Power', unit: 'W', deviceClass: 'power' },
|
||
|
|
energy: { name: 'Energy', unit: 'kWh', deviceClass: 'energy', stateClass: 'total_increasing' },
|
||
|
|
communication_quality: { name: 'Communication quality' },
|
||
|
|
};
|
||
|
|
|
||
|
|
export class BoschShcMapper {
|
||
|
|
public static toDevices(snapshotArg: IBoschShcSnapshot): plugins.shxInterfaces.data.IDeviceDefinition[] {
|
||
|
|
const updatedAt = snapshotArg.updatedAt || new Date().toISOString();
|
||
|
|
const controllerId = this.controllerDeviceId(snapshotArg);
|
||
|
|
const devices: plugins.shxInterfaces.data.IDeviceDefinition[] = [{
|
||
|
|
id: controllerId,
|
||
|
|
integrationDomain: 'bosch_shc',
|
||
|
|
name: snapshotArg.name || snapshotArg.information?.shcIpAddress || 'Bosch Smart Home Controller',
|
||
|
|
protocol: 'http',
|
||
|
|
manufacturer: 'Bosch',
|
||
|
|
model: 'SmartHomeController',
|
||
|
|
online: snapshotArg.online !== false,
|
||
|
|
features: [
|
||
|
|
{ id: 'connectivity', capability: 'sensor', name: 'Connectivity', readable: true, writable: false },
|
||
|
|
],
|
||
|
|
state: [
|
||
|
|
{ featureId: 'connectivity', value: snapshotArg.online === false ? 'offline' : 'online', updatedAt },
|
||
|
|
],
|
||
|
|
metadata: {
|
||
|
|
host: snapshotArg.host || snapshotArg.information?.shcIpAddress,
|
||
|
|
uniqueId: this.controllerUniqueId(snapshotArg),
|
||
|
|
macAddress: snapshotArg.information?.macAddress,
|
||
|
|
softwareVersion: snapshotArg.information?.softwareUpdateState?.swInstalledVersion,
|
||
|
|
updateState: snapshotArg.information?.softwareUpdateState?.swUpdateState,
|
||
|
|
},
|
||
|
|
}];
|
||
|
|
|
||
|
|
const featuresByDevice = new Map<string, TBoschShcFeature[]>();
|
||
|
|
for (const feature of this.features(snapshotArg)) {
|
||
|
|
const entries = featuresByDevice.get(feature.deviceId) || [];
|
||
|
|
entries.push(feature);
|
||
|
|
featuresByDevice.set(feature.deviceId, entries);
|
||
|
|
}
|
||
|
|
|
||
|
|
for (const device of snapshotArg.devices) {
|
||
|
|
if (device.deleted) continue;
|
||
|
|
const featureEntries = featuresByDevice.get(device.id) || [];
|
||
|
|
const shxFeatures: plugins.shxInterfaces.data.IDeviceFeature[] = [
|
||
|
|
{ id: 'connectivity', capability: 'sensor', name: 'Connectivity', readable: true, writable: false },
|
||
|
|
];
|
||
|
|
const state: plugins.shxInterfaces.data.IDeviceState[] = [
|
||
|
|
{ featureId: 'connectivity', value: this.available(device) ? 'online' : 'offline', updatedAt },
|
||
|
|
];
|
||
|
|
|
||
|
|
for (const feature of featureEntries) {
|
||
|
|
if (feature.platform === 'sensor') {
|
||
|
|
shxFeatures.push({ id: feature.id, capability: 'sensor', name: feature.name, readable: true, writable: false, unit: feature.unit });
|
||
|
|
state.push({ featureId: feature.id, value: feature.nativeValue, updatedAt });
|
||
|
|
} else if (feature.platform === 'binary_sensor') {
|
||
|
|
shxFeatures.push({ id: feature.id, capability: 'sensor', name: feature.name, readable: true, writable: false });
|
||
|
|
state.push({ featureId: feature.id, value: feature.isOn, updatedAt });
|
||
|
|
} else if (feature.platform === 'switch') {
|
||
|
|
shxFeatures.push({ id: feature.id, capability: 'switch', name: feature.name, readable: true, writable: true });
|
||
|
|
state.push({ featureId: feature.id, value: feature.isOn, updatedAt });
|
||
|
|
} else if (feature.platform === 'light') {
|
||
|
|
shxFeatures.push({ id: feature.id, capability: 'light', name: feature.name, readable: true, writable: true });
|
||
|
|
state.push({ featureId: feature.id, value: feature.isOn, updatedAt });
|
||
|
|
if (feature.supportsBrightness) {
|
||
|
|
shxFeatures.push({ id: `${feature.id}_brightness`, capability: 'light', name: `${feature.name} brightness`, readable: true, writable: true, unit: '%' });
|
||
|
|
state.push({ featureId: `${feature.id}_brightness`, value: feature.brightness ?? null, updatedAt });
|
||
|
|
}
|
||
|
|
} else if (feature.platform === 'cover') {
|
||
|
|
shxFeatures.push({ id: feature.id, capability: 'cover', name: feature.name, readable: true, writable: true, unit: '%' });
|
||
|
|
state.push({ featureId: feature.id, value: feature.state, updatedAt });
|
||
|
|
if (feature.position !== null) {
|
||
|
|
shxFeatures.push({ id: `${feature.id}_position`, capability: 'cover', name: `${feature.name} position`, readable: true, writable: true, unit: '%' });
|
||
|
|
state.push({ featureId: `${feature.id}_position`, value: feature.position, updatedAt });
|
||
|
|
}
|
||
|
|
} else if (feature.platform === 'climate') {
|
||
|
|
shxFeatures.push({ id: feature.id, capability: 'climate', name: feature.name, readable: true, writable: true });
|
||
|
|
state.push({ featureId: feature.id, value: feature.hvacMode, updatedAt });
|
||
|
|
if (feature.targetTemperature !== undefined) {
|
||
|
|
shxFeatures.push({ id: `${feature.id}_target_temperature`, capability: 'climate', name: `${feature.name} target temperature`, readable: true, writable: true, unit: 'C' });
|
||
|
|
state.push({ featureId: `${feature.id}_target_temperature`, value: feature.targetTemperature, updatedAt });
|
||
|
|
}
|
||
|
|
if (feature.currentTemperature !== undefined) {
|
||
|
|
shxFeatures.push({ id: `${feature.id}_current_temperature`, capability: 'climate', name: `${feature.name} current temperature`, readable: true, writable: false, unit: 'C' });
|
||
|
|
state.push({ featureId: `${feature.id}_current_temperature`, value: feature.currentTemperature, updatedAt });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
devices.push({
|
||
|
|
id: this.deviceId(device),
|
||
|
|
integrationDomain: 'bosch_shc',
|
||
|
|
name: device.name || device.id,
|
||
|
|
protocol: 'http',
|
||
|
|
manufacturer: this.manufacturer(device),
|
||
|
|
model: device.deviceModel || 'Bosch SHC device',
|
||
|
|
online: this.available(device),
|
||
|
|
features: shxFeatures,
|
||
|
|
state,
|
||
|
|
metadata: {
|
||
|
|
rootDeviceId: device.rootDeviceId,
|
||
|
|
serial: device.serial,
|
||
|
|
profile: device.profile,
|
||
|
|
roomId: device.roomId,
|
||
|
|
status: device.status,
|
||
|
|
viaDevice: controllerId,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
return devices;
|
||
|
|
}
|
||
|
|
|
||
|
|
public static toEntities(snapshotArg: IBoschShcSnapshot): IIntegrationEntity[] {
|
||
|
|
return this.features(snapshotArg).map((featureArg) => {
|
||
|
|
const base = {
|
||
|
|
id: this.entityId(featureArg),
|
||
|
|
uniqueId: featureArg.uniqueId,
|
||
|
|
integrationDomain: 'bosch_shc',
|
||
|
|
deviceId: this.deviceId(featureArg.device),
|
||
|
|
platform: featureArg.platform,
|
||
|
|
name: featureArg.name,
|
||
|
|
available: featureArg.available,
|
||
|
|
};
|
||
|
|
|
||
|
|
if (featureArg.platform === 'sensor') {
|
||
|
|
return {
|
||
|
|
...base,
|
||
|
|
state: featureArg.nativeValue,
|
||
|
|
attributes: {
|
||
|
|
unit: featureArg.unit,
|
||
|
|
deviceClass: featureArg.deviceClass,
|
||
|
|
stateClass: featureArg.stateClass,
|
||
|
|
...featureArg.attributes,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
if (featureArg.platform === 'binary_sensor') {
|
||
|
|
return {
|
||
|
|
...base,
|
||
|
|
state: featureArg.isOn ? 'on' : 'off',
|
||
|
|
attributes: {
|
||
|
|
deviceClass: featureArg.deviceClass,
|
||
|
|
...featureArg.attributes,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
if (featureArg.platform === 'switch') {
|
||
|
|
return {
|
||
|
|
...base,
|
||
|
|
state: featureArg.isOn ? 'on' : 'off',
|
||
|
|
attributes: {
|
||
|
|
deviceClass: featureArg.deviceClass,
|
||
|
|
entityCategory: featureArg.entityCategory,
|
||
|
|
switchKind: featureArg.kind,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
if (featureArg.platform === 'light') {
|
||
|
|
return {
|
||
|
|
...base,
|
||
|
|
state: featureArg.isOn ? 'on' : 'off',
|
||
|
|
attributes: {
|
||
|
|
brightness: featureArg.brightness,
|
||
|
|
colorTemperature: featureArg.colorTemperature,
|
||
|
|
minColorTemperature: featureArg.minColorTemperature,
|
||
|
|
maxColorTemperature: featureArg.maxColorTemperature,
|
||
|
|
rgb: featureArg.rgb,
|
||
|
|
supportsBrightness: featureArg.supportsBrightness,
|
||
|
|
supportsColorTemperature: featureArg.supportsColorTemperature,
|
||
|
|
supportsRgb: featureArg.supportsRgb,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
if (featureArg.platform === 'cover') {
|
||
|
|
return {
|
||
|
|
...base,
|
||
|
|
state: featureArg.state,
|
||
|
|
attributes: {
|
||
|
|
currentPosition: featureArg.position,
|
||
|
|
deviceClass: featureArg.deviceClass,
|
||
|
|
operationState: featureArg.operationState,
|
||
|
|
calibrated: featureArg.calibrated,
|
||
|
|
supportsOpen: featureArg.supportsOpen,
|
||
|
|
supportsClose: featureArg.supportsClose,
|
||
|
|
supportsStop: featureArg.supportsStop,
|
||
|
|
supportsPosition: featureArg.supportsPosition,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
return {
|
||
|
|
...base,
|
||
|
|
state: featureArg.hvacMode,
|
||
|
|
attributes: {
|
||
|
|
currentTemperature: featureArg.currentTemperature,
|
||
|
|
targetTemperature: featureArg.targetTemperature,
|
||
|
|
operationMode: featureArg.operationMode,
|
||
|
|
boostMode: featureArg.boostMode,
|
||
|
|
low: featureArg.low,
|
||
|
|
summerMode: featureArg.summerMode,
|
||
|
|
supportsBoostMode: featureArg.supportsBoostMode,
|
||
|
|
supportedHvacModes: featureArg.supportedHvacModes,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
public static features(snapshotArg: IBoschShcSnapshot): TBoschShcFeature[] {
|
||
|
|
return [
|
||
|
|
...this.sensorFeatures(snapshotArg),
|
||
|
|
...this.binarySensorFeatures(snapshotArg),
|
||
|
|
...this.switchFeatures(snapshotArg),
|
||
|
|
...this.lightFeatures(snapshotArg),
|
||
|
|
...this.coverFeatures(snapshotArg),
|
||
|
|
...this.climateFeatures(snapshotArg),
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
public static commandForService(snapshotArg: IBoschShcSnapshot, requestArg: IServiceCallRequest): IBoschShcModeledCommand | { error: string } | undefined {
|
||
|
|
if (requestArg.domain === 'switch') {
|
||
|
|
return this.switchCommand(snapshotArg, requestArg);
|
||
|
|
}
|
||
|
|
if (requestArg.domain === 'light') {
|
||
|
|
return this.lightCommand(snapshotArg, requestArg);
|
||
|
|
}
|
||
|
|
if (requestArg.domain === 'cover') {
|
||
|
|
return this.coverCommand(snapshotArg, requestArg);
|
||
|
|
}
|
||
|
|
if (requestArg.domain === 'climate') {
|
||
|
|
return this.climateCommand(snapshotArg, requestArg);
|
||
|
|
}
|
||
|
|
return undefined;
|
||
|
|
}
|
||
|
|
|
||
|
|
public static controllerDeviceId(snapshotArg: IBoschShcSnapshot): string {
|
||
|
|
return `bosch_shc.controller.${this.slug(this.controllerUniqueId(snapshotArg) || snapshotArg.host || 'controller')}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
public static deviceId(deviceArg: IBoschShcDevice): string {
|
||
|
|
return `bosch_shc.device.${this.slug(deviceArg.id || deviceArg.serial || deviceArg.name || 'device')}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
public static entityId(featureArg: TBoschShcFeature): string {
|
||
|
|
const deviceSlug = this.slug(featureArg.device.name || featureArg.device.id || 'bosch_shc');
|
||
|
|
const alias = this.slug(featureArg.alias);
|
||
|
|
const objectId = this.usesDefaultEntityObjectId(featureArg) ? deviceSlug : `${deviceSlug}_${alias}`;
|
||
|
|
return `${featureArg.platform}.${objectId}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static sensorFeatures(snapshotArg: IBoschShcSnapshot): IBoschShcSensorFeature[] {
|
||
|
|
const features: IBoschShcSensorFeature[] = [];
|
||
|
|
for (const device of this.activeDevices(snapshotArg)) {
|
||
|
|
const model = device.deviceModel || '';
|
||
|
|
const temperature = this.service(snapshotArg, device, 'TemperatureLevel');
|
||
|
|
if (temperature && (thermostatModels.has(model) || wallThermostatModels.has(model))) {
|
||
|
|
features.push(this.sensorFeature(device, 'temperature', this.numberState(temperature, 'temperature'), temperature));
|
||
|
|
}
|
||
|
|
|
||
|
|
const humidity = this.service(snapshotArg, device, 'HumidityLevel');
|
||
|
|
if (humidity && wallThermostatModels.has(model)) {
|
||
|
|
features.push(this.sensorFeature(device, 'humidity', this.numberState(humidity, 'humidity'), humidity));
|
||
|
|
}
|
||
|
|
|
||
|
|
const valve = this.service(snapshotArg, device, 'ValveTappet');
|
||
|
|
if (valve && thermostatModels.has(model)) {
|
||
|
|
features.push(this.sensorFeature(device, 'valvetappet', this.numberState(valve, 'position'), valve, {
|
||
|
|
valve_tappet_state: this.stringState(valve, 'value'),
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
const airQuality = this.service(snapshotArg, device, 'AirQualityLevel');
|
||
|
|
if (airQuality && model === 'TWINGUARD') {
|
||
|
|
features.push(this.sensorFeature(device, 'temperature', this.numberState(airQuality, 'temperature'), airQuality));
|
||
|
|
features.push(this.sensorFeature(device, 'humidity', this.numberState(airQuality, 'humidity'), airQuality));
|
||
|
|
features.push(this.sensorFeature(device, 'purity', this.numberState(airQuality, 'purity'), airQuality));
|
||
|
|
features.push(this.sensorFeature(device, 'airquality', this.stringState(airQuality, 'combinedRating'), airQuality, {
|
||
|
|
rating_description: this.stringState(airQuality, 'description'),
|
||
|
|
}));
|
||
|
|
features.push(this.sensorFeature(device, 'temperature_rating', this.stringState(airQuality, 'temperatureRating'), airQuality));
|
||
|
|
features.push(this.sensorFeature(device, 'humidity_rating', this.stringState(airQuality, 'humidityRating'), airQuality));
|
||
|
|
features.push(this.sensorFeature(device, 'purity_rating', this.stringState(airQuality, 'purityRating'), airQuality));
|
||
|
|
}
|
||
|
|
|
||
|
|
const powerMeter = this.service(snapshotArg, device, 'PowerMeter');
|
||
|
|
if (powerMeter && (smartPlugModels.has(model) || model === 'BSM' || this.hasService(device, 'PowerSwitch'))) {
|
||
|
|
features.push(this.sensorFeature(device, 'power', this.numberState(powerMeter, 'powerConsumption'), powerMeter));
|
||
|
|
const energyWh = this.numberState(powerMeter, 'energyConsumption');
|
||
|
|
features.push(this.sensorFeature(device, 'energy', typeof energyWh === 'number' ? energyWh / 1000 : null, powerMeter));
|
||
|
|
}
|
||
|
|
|
||
|
|
const communicationQuality = this.service(snapshotArg, device, 'CommunicationQuality');
|
||
|
|
if (communicationQuality && smartPlugModels.has(model)) {
|
||
|
|
features.push(this.sensorFeature(device, 'communication_quality', this.stringState(communicationQuality, 'quality'), communicationQuality));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return features;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static binarySensorFeatures(snapshotArg: IBoschShcSnapshot): IBoschShcBinarySensorFeature[] {
|
||
|
|
const features: IBoschShcBinarySensorFeature[] = [];
|
||
|
|
for (const device of this.activeDevices(snapshotArg)) {
|
||
|
|
const model = device.deviceModel || '';
|
||
|
|
const shutterContact = this.service(snapshotArg, device, 'ShutterContact');
|
||
|
|
if (shutterContact && shutterContactModels.has(model)) {
|
||
|
|
const profile = device.profile || '';
|
||
|
|
features.push({
|
||
|
|
...this.featureBase(device, 'binary_sensor', 'contact', this.uniqueId(device, 'contact', true), device.name || 'Shutter contact'),
|
||
|
|
isOn: this.stringState(shutterContact, 'value') === 'OPEN',
|
||
|
|
deviceClass: profile === 'ENTRANCE_DOOR' || profile === 'FRENCH_WINDOW' ? 'door' : 'window',
|
||
|
|
attributes: { profile },
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
const battery = this.service(snapshotArg, device, 'BatteryLevel');
|
||
|
|
if (battery && (batteryModels.has(model) || this.hasService(device, 'BatteryLevel'))) {
|
||
|
|
const level = this.batteryWarningLevel(battery);
|
||
|
|
features.push({
|
||
|
|
...this.featureBase(device, 'binary_sensor', 'battery', this.uniqueId(device, 'battery'), `${device.name || 'Bosch SHC device'} battery`),
|
||
|
|
isOn: level !== 'OK',
|
||
|
|
deviceClass: 'battery',
|
||
|
|
attributes: { warningLevel: level },
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
const leakage = this.service(snapshotArg, device, 'WaterLeakageSensor');
|
||
|
|
if (leakage) {
|
||
|
|
features.push({
|
||
|
|
...this.featureBase(device, 'binary_sensor', 'water_leakage', this.uniqueId(device, 'water_leakage'), `${device.name || 'Bosch SHC device'} water leakage`),
|
||
|
|
isOn: this.stringState(leakage, 'state') === 'LEAKAGE_DETECTED',
|
||
|
|
deviceClass: 'moisture',
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return features;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static switchFeatures(snapshotArg: IBoschShcSnapshot): IBoschShcSwitchFeature[] {
|
||
|
|
const features: IBoschShcSwitchFeature[] = [];
|
||
|
|
for (const device of this.activeDevices(snapshotArg)) {
|
||
|
|
const model = device.deviceModel || '';
|
||
|
|
const powerSwitch = this.service(snapshotArg, device, 'PowerSwitch');
|
||
|
|
if (powerSwitch && !lightModels.has(model)) {
|
||
|
|
const isOutlet = smartPlugModels.has(model);
|
||
|
|
features.push({
|
||
|
|
...this.featureBase(device, 'switch', 'power', this.uniqueId(device, 'power', true), device.name || 'Bosch SHC switch'),
|
||
|
|
kind: 'power_switch',
|
||
|
|
isOn: this.stringState(powerSwitch, 'switchState') === 'ON',
|
||
|
|
deviceClass: isOutlet ? 'outlet' : 'switch',
|
||
|
|
serviceId: 'PowerSwitch',
|
||
|
|
stateKey: 'switchState',
|
||
|
|
onValue: 'ON',
|
||
|
|
offValue: 'OFF',
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
const routing = this.service(snapshotArg, device, 'Routing');
|
||
|
|
if (routing && model === 'PSM') {
|
||
|
|
features.push({
|
||
|
|
...this.featureBase(device, 'switch', 'routing', this.uniqueId(device, 'routing'), `${device.name || 'Bosch SHC device'} routing`),
|
||
|
|
kind: 'routing',
|
||
|
|
isOn: this.stringState(routing, 'value') === 'ENABLED',
|
||
|
|
deviceClass: 'switch',
|
||
|
|
serviceId: 'Routing',
|
||
|
|
stateKey: 'value',
|
||
|
|
onValue: 'ENABLED',
|
||
|
|
offValue: 'DISABLED',
|
||
|
|
entityCategory: 'config',
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
const cameraLight = this.service(snapshotArg, device, 'CameraLight');
|
||
|
|
if (cameraLight && model === 'CAMERA_EYES') {
|
||
|
|
features.push({
|
||
|
|
...this.featureBase(device, 'switch', 'camera_light', this.uniqueId(device, 'camera_light'), `${device.name || 'Bosch SHC camera'} light`),
|
||
|
|
kind: 'camera_light',
|
||
|
|
isOn: this.stringState(cameraLight, 'value') === 'ON',
|
||
|
|
deviceClass: 'switch',
|
||
|
|
serviceId: 'CameraLight',
|
||
|
|
stateKey: 'value',
|
||
|
|
onValue: 'ON',
|
||
|
|
offValue: 'OFF',
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
const privacy = this.service(snapshotArg, device, 'PrivacyMode');
|
||
|
|
if (privacy && model === 'CAMERA_360') {
|
||
|
|
features.push({
|
||
|
|
...this.featureBase(device, 'switch', 'privacy_mode', this.uniqueId(device, 'privacy_mode'), `${device.name || 'Bosch SHC camera'} privacy mode`),
|
||
|
|
kind: 'privacy_mode',
|
||
|
|
isOn: this.stringState(privacy, 'value') === 'DISABLED',
|
||
|
|
deviceClass: 'switch',
|
||
|
|
serviceId: 'PrivacyMode',
|
||
|
|
stateKey: 'value',
|
||
|
|
onValue: 'DISABLED',
|
||
|
|
offValue: 'ENABLED',
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return features.filter((featureArg) => lightSwitchModels.has(featureArg.device.deviceModel || '') || smartPlugModels.has(featureArg.device.deviceModel || '') || featureArg.kind !== 'power_switch');
|
||
|
|
}
|
||
|
|
|
||
|
|
private static lightFeatures(snapshotArg: IBoschShcSnapshot): IBoschShcLightFeature[] {
|
||
|
|
const features: IBoschShcLightFeature[] = [];
|
||
|
|
for (const device of this.activeDevices(snapshotArg)) {
|
||
|
|
const model = device.deviceModel || '';
|
||
|
|
if (!lightModels.has(model) && !this.service(snapshotArg, device, 'BinarySwitch')) continue;
|
||
|
|
|
||
|
|
const binarySwitch = this.service(snapshotArg, device, 'BinarySwitch');
|
||
|
|
const powerSwitch = this.service(snapshotArg, device, 'PowerSwitch');
|
||
|
|
const switchService = binarySwitch || powerSwitch;
|
||
|
|
if (!switchService) continue;
|
||
|
|
|
||
|
|
const brightness = this.service(snapshotArg, device, 'MultiLevelSwitch');
|
||
|
|
const colorTemperature = this.service(snapshotArg, device, 'HueColorTemperature');
|
||
|
|
const rgb = this.service(snapshotArg, device, 'HSBColorActuator');
|
||
|
|
features.push({
|
||
|
|
...this.featureBase(device, 'light', 'light', this.uniqueId(device, 'light', true), device.name || 'Bosch SHC light'),
|
||
|
|
isOn: binarySwitch ? this.booleanState(binarySwitch, 'on') ?? null : this.stringState(switchService, 'switchState') === 'ON',
|
||
|
|
switchServiceId: binarySwitch ? 'BinarySwitch' : 'PowerSwitch',
|
||
|
|
brightness: brightness ? this.numberState(brightness, 'level') : null,
|
||
|
|
colorTemperature: colorTemperature ? this.numberState(colorTemperature, 'colorTemperature') : null,
|
||
|
|
minColorTemperature: colorTemperature ? this.numberNestedState(colorTemperature, 'colorTemperatureRange', 'minCt') : rgb ? this.numberNestedState(rgb, 'colorTemperatureRange', 'minCt') : null,
|
||
|
|
maxColorTemperature: colorTemperature ? this.numberNestedState(colorTemperature, 'colorTemperatureRange', 'maxCt') : rgb ? this.numberNestedState(rgb, 'colorTemperatureRange', 'maxCt') : null,
|
||
|
|
rgb: rgb ? this.numberState(rgb, 'rgb') : null,
|
||
|
|
supportsBrightness: Boolean(brightness),
|
||
|
|
supportsColorTemperature: Boolean(colorTemperature),
|
||
|
|
supportsRgb: Boolean(rgb),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return features;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static coverFeatures(snapshotArg: IBoschShcSnapshot): IBoschShcCoverFeature[] {
|
||
|
|
const features: IBoschShcCoverFeature[] = [];
|
||
|
|
for (const device of this.activeDevices(snapshotArg)) {
|
||
|
|
const shutter = this.service(snapshotArg, device, 'ShutterControl');
|
||
|
|
if (!shutter) continue;
|
||
|
|
const level = this.numberState(shutter, 'level');
|
||
|
|
const position = typeof level === 'number' ? Math.round(level * 100) : null;
|
||
|
|
const operationState = this.stringState(shutter, 'operationState');
|
||
|
|
const model = device.deviceModel || '';
|
||
|
|
features.push({
|
||
|
|
...this.featureBase(device, 'cover', 'cover', this.uniqueId(device, 'cover', true), device.name || 'Bosch SHC cover'),
|
||
|
|
serviceId: 'ShutterControl',
|
||
|
|
deviceClass: model === 'MICROMODULE_AWNING' ? 'awning' : model === 'MICROMODULE_BLINDS' ? 'blind' : 'shutter',
|
||
|
|
state: this.coverState(operationState, position),
|
||
|
|
position,
|
||
|
|
operationState: operationState ?? undefined,
|
||
|
|
calibrated: this.booleanState(shutter, 'calibrated'),
|
||
|
|
supportsOpen: true,
|
||
|
|
supportsClose: true,
|
||
|
|
supportsStop: true,
|
||
|
|
supportsPosition: true,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return features;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static climateFeatures(snapshotArg: IBoschShcSnapshot): IBoschShcClimateFeature[] {
|
||
|
|
const features: IBoschShcClimateFeature[] = [];
|
||
|
|
for (const device of this.activeDevices(snapshotArg)) {
|
||
|
|
const roomClimate = this.service(snapshotArg, device, 'RoomClimateControl');
|
||
|
|
const heatingCircuit = this.service(snapshotArg, device, 'HeatingCircuit');
|
||
|
|
const climateService = roomClimate || heatingCircuit;
|
||
|
|
if (!climateService) continue;
|
||
|
|
|
||
|
|
const temperature = this.service(snapshotArg, device, 'TemperatureLevel');
|
||
|
|
const summerMode = roomClimate ? this.booleanState(roomClimate, 'summerMode') : undefined;
|
||
|
|
features.push({
|
||
|
|
...this.featureBase(device, 'climate', 'climate', this.uniqueId(device, 'climate', true), device.name || 'Bosch SHC climate'),
|
||
|
|
serviceId: roomClimate ? 'RoomClimateControl' : 'HeatingCircuit',
|
||
|
|
hvacMode: summerMode === true ? 'off' : 'heat',
|
||
|
|
currentTemperature: temperature ? this.numberState(temperature, 'temperature') : null,
|
||
|
|
targetTemperature: this.numberState(climateService, 'setpointTemperature'),
|
||
|
|
operationMode: this.stringState(climateService, 'operationMode') ?? undefined,
|
||
|
|
boostMode: roomClimate ? this.booleanState(roomClimate, 'boostMode') : undefined,
|
||
|
|
low: roomClimate ? this.booleanState(roomClimate, 'low') : undefined,
|
||
|
|
summerMode,
|
||
|
|
supportsBoostMode: roomClimate ? this.booleanState(roomClimate, 'supportsBoostMode') : undefined,
|
||
|
|
supportedHvacModes: ['heat', 'off'],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return features;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static switchCommand(snapshotArg: IBoschShcSnapshot, requestArg: IServiceCallRequest): IBoschShcModeledCommand | { error: string } | undefined {
|
||
|
|
if (!['turn_on', 'turn_off'].includes(requestArg.service)) {
|
||
|
|
return { error: `Unsupported Bosch SHC switch service: ${requestArg.service}` };
|
||
|
|
}
|
||
|
|
const feature = this.resolveFeature(snapshotArg, requestArg, 'switch') as IBoschShcSwitchFeature | undefined;
|
||
|
|
if (!feature) {
|
||
|
|
return { error: 'Bosch SHC switch service calls require a switch entity target, or an unambiguous switch device target.' };
|
||
|
|
}
|
||
|
|
const service = this.service(snapshotArg, feature.device, feature.serviceId);
|
||
|
|
if (!service) return { error: `Bosch SHC switch service ${feature.serviceId} is missing from snapshot.` };
|
||
|
|
const value = requestArg.service === 'turn_on' ? feature.onValue : feature.offValue;
|
||
|
|
return this.putServiceState(feature.device, service, { [feature.stateKey]: value }, requestArg);
|
||
|
|
}
|
||
|
|
|
||
|
|
private static lightCommand(snapshotArg: IBoschShcSnapshot, requestArg: IServiceCallRequest): IBoschShcModeledCommand | { error: string } | undefined {
|
||
|
|
if (!['turn_on', 'turn_off'].includes(requestArg.service)) {
|
||
|
|
return { error: `Unsupported Bosch SHC light service: ${requestArg.service}` };
|
||
|
|
}
|
||
|
|
const feature = this.resolveFeature(snapshotArg, requestArg, 'light') as IBoschShcLightFeature | undefined;
|
||
|
|
if (!feature) {
|
||
|
|
return { error: 'Bosch SHC light service calls require a light entity target, or an unambiguous light device target.' };
|
||
|
|
}
|
||
|
|
|
||
|
|
const switchService = this.service(snapshotArg, feature.device, feature.switchServiceId);
|
||
|
|
if (!switchService) return { error: `Bosch SHC light service ${feature.switchServiceId} is missing from snapshot.` };
|
||
|
|
|
||
|
|
const patch: Record<string, unknown> = feature.switchServiceId === 'BinarySwitch'
|
||
|
|
? { on: requestArg.service === 'turn_on' }
|
||
|
|
: { switchState: requestArg.service === 'turn_on' ? 'ON' : 'OFF' };
|
||
|
|
if (requestArg.service === 'turn_on') {
|
||
|
|
const brightness = requestArg.data?.brightness;
|
||
|
|
const brightnessPct = requestArg.data?.brightness_pct;
|
||
|
|
if (brightness !== undefined && !this.isByte(brightness)) {
|
||
|
|
return { error: 'Bosch SHC light brightness must be an integer between 0 and 255.' };
|
||
|
|
}
|
||
|
|
if (brightnessPct !== undefined && !this.isPercent(brightnessPct)) {
|
||
|
|
return { error: 'Bosch SHC light brightness_pct must be an integer between 0 and 100.' };
|
||
|
|
}
|
||
|
|
if (brightness !== undefined || brightnessPct !== undefined) {
|
||
|
|
const brightnessService = this.service(snapshotArg, feature.device, 'MultiLevelSwitch');
|
||
|
|
if (!brightnessService) return { error: 'Bosch SHC light brightness is not supported by this device.' };
|
||
|
|
const level = typeof brightnessPct === 'number' ? brightnessPct : Math.round((brightness as number) * 100 / 255);
|
||
|
|
return this.putServiceState(feature.device, brightnessService, { level }, requestArg);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return this.putServiceState(feature.device, switchService, patch, requestArg);
|
||
|
|
}
|
||
|
|
|
||
|
|
private static coverCommand(snapshotArg: IBoschShcSnapshot, requestArg: IServiceCallRequest): IBoschShcModeledCommand | { error: string } | undefined {
|
||
|
|
const supported = ['open_cover', 'close_cover', 'stop_cover', 'set_cover_position'];
|
||
|
|
if (!supported.includes(requestArg.service)) {
|
||
|
|
return { error: `Unsupported Bosch SHC cover service: ${requestArg.service}` };
|
||
|
|
}
|
||
|
|
const feature = this.resolveFeature(snapshotArg, requestArg, 'cover') as IBoschShcCoverFeature | undefined;
|
||
|
|
if (!feature) {
|
||
|
|
return { error: 'Bosch SHC cover service calls require a cover entity target, or an unambiguous cover device target.' };
|
||
|
|
}
|
||
|
|
const service = this.service(snapshotArg, feature.device, 'ShutterControl');
|
||
|
|
if (!service) return { error: 'Bosch SHC ShutterControl service is missing from snapshot.' };
|
||
|
|
|
||
|
|
if (requestArg.service === 'open_cover') return this.putServiceState(feature.device, service, { level: 1.0 }, requestArg);
|
||
|
|
if (requestArg.service === 'close_cover') return this.putServiceState(feature.device, service, { level: 0.0 }, requestArg);
|
||
|
|
if (requestArg.service === 'stop_cover') return this.putServiceState(feature.device, service, { operationState: 'STOPPED' }, requestArg);
|
||
|
|
const position = requestArg.data?.position;
|
||
|
|
if (!this.isPercent(position)) {
|
||
|
|
return { error: 'Bosch SHC set_cover_position requires data.position as an integer between 0 and 100.' };
|
||
|
|
}
|
||
|
|
return this.putServiceState(feature.device, service, { level: position / 100 }, requestArg);
|
||
|
|
}
|
||
|
|
|
||
|
|
private static climateCommand(snapshotArg: IBoschShcSnapshot, requestArg: IServiceCallRequest): IBoschShcModeledCommand | { error: string } | undefined {
|
||
|
|
if (requestArg.service !== 'set_temperature') {
|
||
|
|
return { error: `Unsupported Bosch SHC climate service: ${requestArg.service}` };
|
||
|
|
}
|
||
|
|
const feature = this.resolveFeature(snapshotArg, requestArg, 'climate') as IBoschShcClimateFeature | undefined;
|
||
|
|
if (!feature) {
|
||
|
|
return { error: 'Bosch SHC climate service calls require a climate entity target, or an unambiguous climate device target.' };
|
||
|
|
}
|
||
|
|
const temperature = requestArg.data?.temperature;
|
||
|
|
if (typeof temperature !== 'number' || !Number.isFinite(temperature)) {
|
||
|
|
return { error: 'Bosch SHC set_temperature requires data.temperature as a finite number.' };
|
||
|
|
}
|
||
|
|
const service = this.service(snapshotArg, feature.device, feature.serviceId);
|
||
|
|
if (!service) return { error: `Bosch SHC climate service ${feature.serviceId} is missing from snapshot.` };
|
||
|
|
return this.putServiceState(feature.device, service, { setpointTemperature: temperature }, requestArg);
|
||
|
|
}
|
||
|
|
|
||
|
|
private static resolveFeature(snapshotArg: IBoschShcSnapshot, requestArg: IServiceCallRequest, platformArg: TBoschShcFeature['platform']): TBoschShcFeature | undefined {
|
||
|
|
const features = this.features(snapshotArg).filter((featureArg) => featureArg.platform === platformArg);
|
||
|
|
if (requestArg.target.entityId) {
|
||
|
|
const entities = this.toEntities(snapshotArg);
|
||
|
|
const entity = entities.find((entityArg) => entityArg.id === requestArg.target.entityId);
|
||
|
|
return features.find((featureArg) => featureArg.uniqueId === entity?.uniqueId);
|
||
|
|
}
|
||
|
|
if (requestArg.target.deviceId) {
|
||
|
|
const deviceFeatures = features.filter((featureArg) => this.deviceId(featureArg.device) === requestArg.target.deviceId);
|
||
|
|
return deviceFeatures.length === 1 ? deviceFeatures[0] : undefined;
|
||
|
|
}
|
||
|
|
return undefined;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static putServiceState(deviceArg: IBoschShcDevice, serviceArg: IBoschShcDeviceService, patchArg: Record<string, unknown>, requestArg: IServiceCallRequest): IBoschShcModeledCommand {
|
||
|
|
const stateType = typeof serviceArg.state?.['@type'] === 'string' ? serviceArg.state['@type'] : undefined;
|
||
|
|
return {
|
||
|
|
action: 'put_device_service_state',
|
||
|
|
method: 'PUT',
|
||
|
|
path: `/devices/${this.encodeDeviceId(deviceArg.id)}/services/${serviceArg.id}/state`,
|
||
|
|
body: stateType ? { '@type': stateType, ...patchArg } : patchArg,
|
||
|
|
deviceId: deviceArg.id,
|
||
|
|
serviceId: serviceArg.id,
|
||
|
|
domain: requestArg.domain,
|
||
|
|
service: requestArg.service,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
private static sensorFeature(deviceArg: IBoschShcDevice, keyArg: string, valueArg: string | number | null, serviceArg: IBoschShcDeviceService, attributesArg: Record<string, unknown> = {}): IBoschShcSensorFeature {
|
||
|
|
const metadata = sensorMetadata[keyArg] || { name: this.humanize(keyArg) };
|
||
|
|
void serviceArg;
|
||
|
|
return {
|
||
|
|
...this.featureBase(deviceArg, 'sensor', keyArg, this.uniqueId(deviceArg, keyArg), `${deviceArg.name || 'Bosch SHC device'} ${metadata.name.toLowerCase()}`),
|
||
|
|
nativeValue: valueArg,
|
||
|
|
unit: metadata.unit,
|
||
|
|
deviceClass: metadata.deviceClass,
|
||
|
|
stateClass: metadata.stateClass,
|
||
|
|
attributes: attributesArg,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
private static featureBase<TPlatform extends TBoschShcFeature['platform']>(deviceArg: IBoschShcDevice, platformArg: TPlatform, aliasArg: string, uniqueIdArg: string, nameArg: string): IBoschShcFeatureBase & { platform: TPlatform } {
|
||
|
|
return {
|
||
|
|
platform: platformArg,
|
||
|
|
id: `${platformArg}_${this.slug(aliasArg)}`,
|
||
|
|
alias: aliasArg,
|
||
|
|
uniqueId: uniqueIdArg,
|
||
|
|
name: nameArg,
|
||
|
|
deviceId: deviceArg.id,
|
||
|
|
device: deviceArg,
|
||
|
|
available: this.available(deviceArg),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
private static service(snapshotArg: IBoschShcSnapshot, deviceArg: IBoschShcDevice, serviceIdArg: string): IBoschShcDeviceService | undefined {
|
||
|
|
return deviceArg.services?.find((serviceArg) => serviceArg.id === serviceIdArg)
|
||
|
|
|| snapshotArg.services?.find((serviceArg) => serviceArg.deviceId === deviceArg.id && serviceArg.id === serviceIdArg);
|
||
|
|
}
|
||
|
|
|
||
|
|
private static activeDevices(snapshotArg: IBoschShcSnapshot): IBoschShcDevice[] {
|
||
|
|
return snapshotArg.devices.filter((deviceArg) => !deviceArg.deleted);
|
||
|
|
}
|
||
|
|
|
||
|
|
private static hasService(deviceArg: IBoschShcDevice, serviceIdArg: string): boolean {
|
||
|
|
return Boolean(deviceArg.deviceServiceIds?.includes(serviceIdArg) || deviceArg.services?.some((serviceArg) => serviceArg.id === serviceIdArg));
|
||
|
|
}
|
||
|
|
|
||
|
|
private static stringState(serviceArg: IBoschShcDeviceService, keyArg: string): string | null {
|
||
|
|
const value = serviceArg.state?.[keyArg];
|
||
|
|
return typeof value === 'string' ? value : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static numberState(serviceArg: IBoschShcDeviceService, keyArg: string): number | null {
|
||
|
|
const value = serviceArg.state?.[keyArg];
|
||
|
|
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
||
|
|
if (typeof value === 'string' && value.trim()) {
|
||
|
|
const numberValue = Number(value);
|
||
|
|
return Number.isFinite(numberValue) ? numberValue : null;
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static numberNestedState(serviceArg: IBoschShcDeviceService, objectKeyArg: string, keyArg: string): number | null {
|
||
|
|
const record = asRecord(serviceArg.state?.[objectKeyArg]);
|
||
|
|
const value = record?.[keyArg];
|
||
|
|
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static booleanState(serviceArg: IBoschShcDeviceService, keyArg: string): boolean | undefined {
|
||
|
|
const value = serviceArg.state?.[keyArg];
|
||
|
|
return typeof value === 'boolean' ? value : undefined;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static batteryWarningLevel(serviceArg: IBoschShcDeviceService): string {
|
||
|
|
const entry = serviceArg.faults?.entries?.[0];
|
||
|
|
return typeof entry?.type === 'string' ? entry.type : 'OK';
|
||
|
|
}
|
||
|
|
|
||
|
|
private static coverState(operationStateArg: string | null, positionArg: number | null): TBoschShcCoverState {
|
||
|
|
if (operationStateArg === 'OPENING') return 'opening';
|
||
|
|
if (operationStateArg === 'CLOSING') return 'closing';
|
||
|
|
if (positionArg === 0) return 'closed';
|
||
|
|
if (typeof positionArg === 'number' && positionArg > 0) return 'open';
|
||
|
|
if (operationStateArg === 'STOPPED') return 'stopped';
|
||
|
|
return 'unknown';
|
||
|
|
}
|
||
|
|
|
||
|
|
private static available(deviceArg: IBoschShcDevice): boolean {
|
||
|
|
return deviceArg.status === undefined || deviceArg.status === 'AVAILABLE';
|
||
|
|
}
|
||
|
|
|
||
|
|
private static manufacturer(deviceArg: IBoschShcDevice): string {
|
||
|
|
const value = deviceArg.manufacturer || 'Bosch';
|
||
|
|
return value.toUpperCase() === 'BOSCH' ? 'Bosch' : value;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static uniqueId(deviceArg: IBoschShcDevice, aliasArg: string, defaultArg = false): string {
|
||
|
|
const base = deviceArg.serial || deviceArg.id;
|
||
|
|
return defaultArg ? base : `${base}_${aliasArg}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static usesDefaultEntityObjectId(featureArg: TBoschShcFeature): boolean {
|
||
|
|
return featureArg.platform === 'binary_sensor' && featureArg.alias === 'contact'
|
||
|
|
|| featureArg.platform === 'switch' && featureArg.alias === 'power'
|
||
|
|
|| featureArg.platform === 'light' && featureArg.alias === 'light'
|
||
|
|
|| featureArg.platform === 'cover' && featureArg.alias === 'cover'
|
||
|
|
|| featureArg.platform === 'climate' && featureArg.alias === 'climate';
|
||
|
|
}
|
||
|
|
|
||
|
|
private static controllerUniqueId(snapshotArg: IBoschShcSnapshot): string | undefined {
|
||
|
|
return formatMac(snapshotArg.uniqueId || snapshotArg.information?.macAddress || '') || snapshotArg.host;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static encodeDeviceId(valueArg: string): string {
|
||
|
|
return valueArg.replace(/#/g, '%23');
|
||
|
|
}
|
||
|
|
|
||
|
|
private static isByte(valueArg: unknown): valueArg is number {
|
||
|
|
return typeof valueArg === 'number' && Number.isInteger(valueArg) && valueArg >= 0 && valueArg <= 255;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static isPercent(valueArg: unknown): valueArg is number {
|
||
|
|
return typeof valueArg === 'number' && Number.isInteger(valueArg) && valueArg >= 0 && valueArg <= 100;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static slug(valueArg: string): string {
|
||
|
|
return valueArg.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '') || 'bosch_shc';
|
||
|
|
}
|
||
|
|
|
||
|
|
private static humanize(valueArg: string): string {
|
||
|
|
return valueArg.replace(/_/g, ' ').replace(/([a-z])([A-Z])/g, '$1 $2').toLowerCase();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export const formatMac = (valueArg?: string): string | undefined => {
|
||
|
|
if (!valueArg) return undefined;
|
||
|
|
const trimmed = valueArg.trim();
|
||
|
|
if (/^[0-9a-f]{2}(-[0-9a-f]{2}){5}$/i.test(trimmed)) return trimmed.toLowerCase();
|
||
|
|
const compact = trimmed.replace(/[:.-]/g, '').toLowerCase();
|
||
|
|
if (!/^[0-9a-f]{12}$/.test(compact)) return undefined;
|
||
|
|
return compact.match(/../g)?.join('-');
|
||
|
|
};
|
||
|
|
|
||
|
|
const asRecord = (valueArg: unknown): Record<string, unknown> | undefined => {
|
||
|
|
return typeof valueArg === 'object' && valueArg !== null && !Array.isArray(valueArg)
|
||
|
|
? valueArg as Record<string, unknown>
|
||
|
|
: undefined;
|
||
|
|
};
|