mirror of
https://github.com/adrianjagielak/home-assistant-futurehome.git
synced 2026-02-11 07:15:38 +00:00
Finish most of the basic features, add few services
This commit is contained in:
154
futurehome/src/fimp/fimp.ts
Normal file
154
futurehome/src/fimp/fimp.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { MqttClient } from "mqtt/*";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { log } from "../logger";
|
||||
|
||||
let fimp: MqttClient | undefined = undefined;
|
||||
|
||||
export function setFimp(client: MqttClient) {
|
||||
fimp = client;
|
||||
}
|
||||
|
||||
export type FimpResponse = {
|
||||
corid?: any;
|
||||
ctime?: any;
|
||||
props?: any;
|
||||
serv?: any;
|
||||
tags?: any;
|
||||
type?: string | null;
|
||||
uid?: any;
|
||||
val?: any;
|
||||
val_t?: string;
|
||||
ver?: any;
|
||||
};
|
||||
|
||||
export async function sendFimpMsg({
|
||||
address,
|
||||
service,
|
||||
cmd,
|
||||
val,
|
||||
val_t,
|
||||
timeoutMs = 10000,
|
||||
}: {
|
||||
address: string;
|
||||
service: string;
|
||||
cmd: string;
|
||||
val: unknown;
|
||||
val_t: string;
|
||||
timeoutMs?: number;
|
||||
}): Promise<FimpResponse> {
|
||||
const uid = uuidv4();
|
||||
const topic = `pt:j1/mt:cmd${address}`;
|
||||
const message = JSON.stringify(
|
||||
{
|
||||
corid: null,
|
||||
ctime: new Date().toISOString(),
|
||||
props: {},
|
||||
resp_to: 'pt:j1/mt:rsp/rt:app/rn:ha-futurehome/ad:addon',
|
||||
serv: service,
|
||||
src: 'ha-futurehome',
|
||||
tags: [],
|
||||
'type': cmd,
|
||||
uid: uid,
|
||||
val: val,
|
||||
val_t: val_t,
|
||||
ver: '1',
|
||||
},
|
||||
);
|
||||
|
||||
// For example for "cmd.foo.set" we would expect to get "evt.foo.report" back (plus the service name must match).
|
||||
let possibleResponseType: string | null = null;
|
||||
if (cmd.split('.').length === 3) {
|
||||
possibleResponseType = cmd.split('.').map(
|
||||
(part, index, array) => index === 0 ? 'evt' : (index === array.length - 1 ? 'report' : part),
|
||||
).join('.');
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
fimp?.removeListener('message', onResponse);
|
||||
let error = new Error(`Timeout waiting for FIMP response (uid: ${uid}, service: ${service}, cmd: ${cmd})`);
|
||||
log.warn(error.message, error.stack);
|
||||
reject(error);
|
||||
}, timeoutMs);
|
||||
|
||||
const onResponse = (topic: string, buffer: any) => {
|
||||
const msg = JSON.parse(buffer.toString());
|
||||
|
||||
if (msg.corid === uid) {
|
||||
if (msg.type === 'evt.error.report') {
|
||||
fimp?.removeListener('message', onResponse);
|
||||
|
||||
let error = new Error(`Received FIMP response for message ${uid}: error (evt.error.report) (matched using uid)`);
|
||||
log.warn(error.message, error.stack);
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug(`Received FIMP response for message ${uid} (matched using uid).`);
|
||||
|
||||
clearTimeout(timeout);
|
||||
fimp?.removeListener('message', onResponse);
|
||||
resolve(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.topic === `pt:j1/mt:evt${address}`) {
|
||||
if (msg.type === 'evt.error.report') {
|
||||
fimp?.removeListener('message', onResponse);
|
||||
|
||||
let error = new Error(`Received FIMP response for message ${uid}: error (evt.error.report) (matched using topic)`);
|
||||
log.warn(error.message, error.stack);
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug(`Received FIMP response for message ${uid} (matched using topic).`);
|
||||
|
||||
clearTimeout(timeout);
|
||||
fimp?.removeListener('message', onResponse);
|
||||
resolve(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO(adrianjagielak): is this needed?
|
||||
// if (possibleResponseType != null && msg.type === possibleResponseType && msg.serv === parameters.service) {
|
||||
// log.debug(`Received FIMP response for message ${uid} (matched using possible response type "${possibleResponseType}").`);
|
||||
//
|
||||
// clearTimeout(timeout);
|
||||
// effectiveMqttClient.removeListener('message', onResponse);
|
||||
// resolve(msg);
|
||||
// return;
|
||||
// }
|
||||
|
||||
const hasValidType = msg.type != null && msg.type.startsWith('evt.');
|
||||
const msgParts = msg.type?.split('.') ?? [];
|
||||
const cmdParts = cmd.split('.');
|
||||
const hasThreeParts = msgParts.length === 3 && cmdParts.length === 3;
|
||||
const middlePartMatches = msgParts[1] === cmdParts[1];
|
||||
const endsWithLastPart = cmd.endsWith(msgParts.at(-1)!);
|
||||
const sameService = msg.serv === service;
|
||||
if (hasValidType && hasThreeParts && middlePartMatches && endsWithLastPart && sameService) {
|
||||
log.debug(`Received FIMP response for message ${uid} (matched using event name).`);
|
||||
|
||||
clearTimeout(timeout);
|
||||
fimp?.removeListener('message', onResponse);
|
||||
resolve(msg);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
fimp?.on('message', onResponse);
|
||||
|
||||
log.debug(`
|
||||
Sending FIMP message:
|
||||
address: "${address}",
|
||||
service: "${service}",
|
||||
uid: "${uid}",
|
||||
cmd: "${cmd}",
|
||||
val: "${JSON.stringify(val)}",
|
||||
val_t: "${val_t}"
|
||||
`);
|
||||
|
||||
fimp?.publish(topic, message, { qos: 1 });
|
||||
});
|
||||
}
|
||||
35
futurehome/src/fimp/helpers.ts
Normal file
35
futurehome/src/fimp/helpers.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/// Returns the adapter address of the device associated with the given service address.
|
||||
/// The service address may belong to any service on the device.
|
||||
export function adapterAddressFromServiceAddress(serviceAddress: string): string {
|
||||
const parts = serviceAddress.split('/');
|
||||
|
||||
if (parts.length < 4) {
|
||||
throw new Error("Invalid address format");
|
||||
}
|
||||
|
||||
const adapterPart = parts[2]; // e.g., "rn:zigbee"
|
||||
const adapterName = adapterPart.split(':')[1]; // "zigbee"
|
||||
const adPart = parts[3]; // e.g., "ad:1"
|
||||
|
||||
return `/rt:ad/rn:${adapterName}/${adPart}`;
|
||||
}
|
||||
|
||||
/// Returns the adapter service name of the device associated with the given service address.
|
||||
/// The service address may belong to any service on the device.
|
||||
export function adapterServiceFromServiceAddress(serviceAddress: string): string {
|
||||
const parts = serviceAddress.split('/');
|
||||
|
||||
if (parts.length < 3) {
|
||||
throw new Error("Invalid address format");
|
||||
}
|
||||
|
||||
const adapterPart = parts[2]; // e.g., "rn:zigbee"
|
||||
const adapterName = adapterPart.split(':')[1]; // "zigbee"
|
||||
|
||||
if (adapterName === 'zw') {
|
||||
return 'zwave-ad';
|
||||
}
|
||||
|
||||
return adapterName;
|
||||
|
||||
}
|
||||
32
futurehome/src/fimp/inclusion_report.ts
Normal file
32
futurehome/src/fimp/inclusion_report.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { sendFimpMsg } from "./fimp";
|
||||
|
||||
export type InclusionReport = {
|
||||
address?: string | null; // Household device ID
|
||||
product_name?: string | null; // e.g. "SWITCH-ZR03-1"
|
||||
product_hash?: string | null; // e.g. "zb - eWeLink - SWITCH-ZR03-1"
|
||||
product_id?: string | null; // e.g. "SWITCH-ZR03-1"
|
||||
manufacturer_id?: string | null; // e.g. "eWeLink"
|
||||
device_id?: string | null; // e.g. "b4:0e:cf:d1:bc:2a:00:00"
|
||||
hw_ver?: string | null; // e.g. "0"
|
||||
sw_ver?: string | null; // e.g. "0x0"
|
||||
comm_tech?: string | null; // e.g. "zigbee"
|
||||
power_source?: string | null; // e.g. "battery"
|
||||
services?: InclusionReportService[] | null;
|
||||
};
|
||||
|
||||
export type InclusionReportService = {
|
||||
name?: string | null;
|
||||
address?: string | null;
|
||||
};
|
||||
|
||||
export async function getInclusionReport(parameters: { adapterAddress: string; adapterService: string; deviceId: string }): Promise<InclusionReport> {
|
||||
const inclusionReport = await sendFimpMsg({
|
||||
address: parameters.adapterAddress,
|
||||
service: parameters.adapterService,
|
||||
cmd: 'cmd.thing.get_inclusion_report',
|
||||
val: parameters.deviceId,
|
||||
val_t: 'string',
|
||||
});
|
||||
|
||||
return inclusionReport.val;
|
||||
}
|
||||
113
futurehome/src/fimp/state.ts
Normal file
113
futurehome/src/fimp/state.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
export type DeviceState = {
|
||||
id?: number | null;
|
||||
services?: DeviceStateService[] | null
|
||||
};
|
||||
|
||||
export type DeviceStateService = {
|
||||
addr?: string;
|
||||
attributes?: Attribute[];
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export type Attribute = {
|
||||
name: string;
|
||||
values: AttributeValue[];
|
||||
}
|
||||
|
||||
export type AttributeValue = StringValue | IntValue | FloatValue | BoolValue | NullValue | StrArrayValue | IntArrayValue | FloatArrayValue | StrMapValue | IntMapValue | FloatMapValue | BoolMapValue | ObjectValue | BinValue;
|
||||
|
||||
export type Timestamp = string;
|
||||
|
||||
export type StringValue = {
|
||||
ts: Timestamp;
|
||||
val: string;
|
||||
val_t: 'string';
|
||||
}
|
||||
|
||||
export type IntValue = {
|
||||
ts: Timestamp;
|
||||
val: number;
|
||||
val_t: 'int';
|
||||
}
|
||||
|
||||
export type FloatValue = {
|
||||
ts: Timestamp;
|
||||
val: number;
|
||||
val_t: 'float';
|
||||
}
|
||||
|
||||
export type BoolValue = {
|
||||
ts: Timestamp;
|
||||
val: boolean;
|
||||
val_t: 'bool';
|
||||
}
|
||||
|
||||
export type NullValue = {
|
||||
ts: Timestamp;
|
||||
val?: null;
|
||||
val_t: 'null';
|
||||
}
|
||||
|
||||
export type StrArrayValue = {
|
||||
ts: Timestamp;
|
||||
val: string[];
|
||||
val_t: 'str_array';
|
||||
}
|
||||
|
||||
export type IntArrayValue = {
|
||||
ts: Timestamp;
|
||||
val: number[];
|
||||
val_t: 'int_array';
|
||||
}
|
||||
|
||||
export type FloatArrayValue = {
|
||||
ts: Timestamp;
|
||||
val: number[];
|
||||
val_t: 'float_array';
|
||||
}
|
||||
|
||||
export type StrMapValue = {
|
||||
ts: Timestamp;
|
||||
val: {
|
||||
[key: string]: string;
|
||||
};
|
||||
val_t: 'str_map';
|
||||
}
|
||||
|
||||
export type IntMapValue = {
|
||||
ts: Timestamp;
|
||||
val: {
|
||||
[key: string]: number;
|
||||
};
|
||||
val_t: 'int_map';
|
||||
}
|
||||
|
||||
export type FloatMapValue = {
|
||||
ts: Timestamp;
|
||||
val: {
|
||||
[key: string]: number;
|
||||
};
|
||||
val_t: 'float_map';
|
||||
}
|
||||
|
||||
export type BoolMapValue = {
|
||||
ts: Timestamp;
|
||||
val: {
|
||||
[key: string]: boolean;
|
||||
};
|
||||
val_t: 'bool_map';
|
||||
}
|
||||
|
||||
export type ObjectValue = {
|
||||
ts: Timestamp;
|
||||
val: {
|
||||
[key: string]: any;
|
||||
};
|
||||
val_t: 'object';
|
||||
}
|
||||
|
||||
export type BinValue = {
|
||||
ts: Timestamp;
|
||||
val: string;
|
||||
val_t: 'bin';
|
||||
}
|
||||
12
futurehome/src/fimp/vinculum_pd7_device.ts
Normal file
12
futurehome/src/fimp/vinculum_pd7_device.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export type VinculumPd7Device = {
|
||||
client?: {
|
||||
// User-defined device name
|
||||
name?: string | null,
|
||||
} | null,
|
||||
id: number,
|
||||
services?: any,
|
||||
type?: {
|
||||
type?: string | null,
|
||||
subtype?: string | null,
|
||||
} | null,
|
||||
};
|
||||
Reference in New Issue
Block a user