135 lines
7.2 KiB
TypeScript
135 lines
7.2 KiB
TypeScript
import type * as shxInterfaces from '@smarthome.exchange/interfaces';
|
|
import { SimpleLocalMapper, type IIntegrationEntity, type ISimpleLocalEntitySnapshot, type ISimpleLocalSnapshot, type ISimpleLocalSnapshotOptions, type TSimpleLocalRawData } from '../../core/index.js';
|
|
import type { IEmonitorConfig } from './emonitor.types.js';
|
|
import { emonitorDefaultName, emonitorProfile } from './emonitor.types.js';
|
|
|
|
export class EmonitorMapper {
|
|
public static toSnapshot(optionsArg: Omit<ISimpleLocalSnapshotOptions<IEmonitorConfig>, 'profile'>): ISimpleLocalSnapshot {
|
|
return SimpleLocalMapper.toSnapshot({
|
|
...optionsArg,
|
|
profile: emonitorProfile,
|
|
rawData: this.normalizeRawData(optionsArg.config, optionsArg.rawData),
|
|
});
|
|
}
|
|
|
|
public static toSnapshotFromRaw(configArg: IEmonitorConfig, rawDataArg: TSimpleLocalRawData): ISimpleLocalSnapshot {
|
|
return this.toSnapshot({ config: configArg, rawData: rawDataArg, online: true, source: 'manual' });
|
|
}
|
|
|
|
public static toDevices(snapshotArg: ISimpleLocalSnapshot): shxInterfaces.data.IDeviceDefinition[] {
|
|
return SimpleLocalMapper.toDevices(emonitorProfile, snapshotArg);
|
|
}
|
|
|
|
public static toEntities(snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] {
|
|
return SimpleLocalMapper.toEntities(emonitorProfile, snapshotArg);
|
|
}
|
|
|
|
public static slug(valueArg: unknown): string {
|
|
return SimpleLocalMapper.slug(valueArg);
|
|
}
|
|
|
|
private static normalizeRawData(configArg: IEmonitorConfig, rawDataArg: unknown): unknown {
|
|
if (!isRecord(rawDataArg) || isSnapshotLike(rawDataArg) || hasSimpleEntities(rawDataArg)) {
|
|
return rawDataArg;
|
|
}
|
|
|
|
const channels = channelRecords(rawDataArg.channels);
|
|
if (!channels.length) {
|
|
return rawDataArg;
|
|
}
|
|
|
|
const network = recordValue(rawDataArg.network) || {};
|
|
const hardware = recordValue(rawDataArg.hardware) || {};
|
|
const macAddress = configArg.macAddress || configArg.mac_address || stringValue(network.mac_address) || stringValue(network.macAddress) || stringValue(rawDataArg.mac_address) || stringValue(rawDataArg.macAddress);
|
|
const serialNumber = stringValue(hardware.serial_number) || stringValue(hardware.serialNumber) || stringValue(rawDataArg.serial_number) || stringValue(rawDataArg.serialNumber);
|
|
const firmwareVersion = stringValue(hardware.firmware_version) || stringValue(hardware.firmwareVersion) || stringValue(rawDataArg.firmware_version) || stringValue(rawDataArg.firmwareVersion);
|
|
const deviceName = configArg.name || stringValue(rawDataArg.name) || (macAddress ? `Emonitor ${shortMac(macAddress)}` : emonitorDefaultName);
|
|
const entities: ISimpleLocalEntitySnapshot[] = [];
|
|
const seenChannels = new Set<string>();
|
|
|
|
for (const channel of channels) {
|
|
seenChannels.add(channel.number);
|
|
if (channel.data.active === false) {
|
|
continue;
|
|
}
|
|
const pairedChannel = stringValue(channel.data.paired_with_channel) || stringValue(channel.data.pairedWithChannel) || numberString(channel.data.paired_with_channel) || numberString(channel.data.pairedWithChannel);
|
|
if (pairedChannel && seenChannels.has(pairedChannel)) {
|
|
continue;
|
|
}
|
|
|
|
const label = stringValue(channel.data.label) || channel.number;
|
|
entities.push(...powerEntities({ channelNumber: channel.number, label, data: channel.data, pairedData: pairedChannel ? channels.find((candidateArg) => candidateArg.number === pairedChannel)?.data : undefined, macAddress, deviceName }));
|
|
}
|
|
|
|
return {
|
|
device: {
|
|
id: configArg.uniqueId || macAddress || serialNumber || configArg.host || deviceName,
|
|
name: deviceName,
|
|
manufacturer: emonitorProfile.manufacturer,
|
|
model: emonitorProfile.model,
|
|
serialNumber,
|
|
host: configArg.host,
|
|
port: configArg.port,
|
|
protocol: emonitorProfile.defaultProtocol,
|
|
attributes: {
|
|
firmwareVersion,
|
|
macAddress,
|
|
},
|
|
},
|
|
entities,
|
|
online: configArg.online ?? true,
|
|
updatedAt: new Date().toISOString(),
|
|
source: 'manual',
|
|
rawData: rawDataArg,
|
|
} satisfies ISimpleLocalSnapshot;
|
|
}
|
|
}
|
|
|
|
const powerEntities = (optionsArg: { channelNumber: string; label: string; data: Record<string, unknown>; pairedData?: Record<string, unknown>; macAddress?: string; deviceName: string }): ISimpleLocalEntitySnapshot[] => {
|
|
const base = SimpleLocalMapper.slug(optionsArg.macAddress || optionsArg.deviceName);
|
|
return [
|
|
['inst_power', optionsArg.label],
|
|
['avg_power', `${optionsArg.label} average`],
|
|
['max_power', `${optionsArg.label} max`],
|
|
].map(([keyArg, nameArg]) => ({
|
|
id: `channel_${SimpleLocalMapper.slug(optionsArg.channelNumber)}_${keyArg}`,
|
|
uniqueId: `${emonitorProfile.domain}_${base}_${SimpleLocalMapper.slug(optionsArg.channelNumber)}_${keyArg}`,
|
|
name: nameArg,
|
|
platform: 'sensor',
|
|
state: sumPower(optionsArg.data, optionsArg.pairedData, keyArg),
|
|
available: true,
|
|
writable: false,
|
|
unit: 'W',
|
|
deviceClass: 'power',
|
|
stateClass: 'measurement',
|
|
attributes: {
|
|
channel: Number(optionsArg.channelNumber),
|
|
pairedWithChannel: numberValue(optionsArg.data.paired_with_channel) ?? numberValue(optionsArg.data.pairedWithChannel),
|
|
},
|
|
}));
|
|
};
|
|
|
|
const sumPower = (channelArg: Record<string, unknown>, pairedChannelArg: Record<string, unknown> | undefined, keyArg: string): number => (numberValue(channelArg[keyArg]) || 0) + (pairedChannelArg ? numberValue(pairedChannelArg[keyArg]) || 0 : 0);
|
|
|
|
const channelRecords = (valueArg: unknown): Array<{ number: string; data: Record<string, unknown> }> => {
|
|
if (Array.isArray(valueArg)) {
|
|
return valueArg.filter(isRecord).map((channelArg, indexArg) => ({ number: stringValue(channelArg.channel) || stringValue(channelArg.channel_number) || String(indexArg + 1), data: channelArg }));
|
|
}
|
|
if (isRecord(valueArg)) {
|
|
return Object.entries(valueArg).filter((entryArg): entryArg is [string, Record<string, unknown>] => isRecord(entryArg[1])).map(([numberArg, dataArg]) => ({ number: numberArg, data: dataArg }));
|
|
}
|
|
return [];
|
|
};
|
|
|
|
const shortMac = (valueArg: string): string => valueArg.replace(/[^a-fA-F0-9]/g, '').slice(-6).toUpperCase();
|
|
const hasSimpleEntities = (valueArg: Record<string, unknown>): boolean => Array.isArray(valueArg.entities) && valueArg.entities.some((entityArg) => isRecord(entityArg) && 'name' in entityArg && 'state' in entityArg);
|
|
const isSnapshotLike = (valueArg: Record<string, unknown>): boolean => isRecord(valueArg.device) && Array.isArray(valueArg.entities);
|
|
const recordValue = (valueArg: unknown): Record<string, unknown> | undefined => isRecord(valueArg) ? valueArg : undefined;
|
|
const isRecord = (valueArg: unknown): valueArg is Record<string, unknown> => Boolean(valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg));
|
|
const stringValue = (valueArg: unknown): string | undefined => typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
|
|
const numberString = (valueArg: unknown): string | undefined => typeof valueArg === 'number' && Number.isFinite(valueArg) ? String(valueArg) : undefined;
|
|
const numberValue = (valueArg: unknown): number | undefined => {
|
|
const value = typeof valueArg === 'number' ? valueArg : typeof valueArg === 'string' ? Number(valueArg) : undefined;
|
|
return value !== undefined && Number.isFinite(value) ? value : undefined;
|
|
};
|