Add native Z-Wave JS integration
This commit is contained in:
@@ -9,6 +9,7 @@ import { RokuIntegration } from './integrations/roku/index.js';
|
||||
import { ShellyIntegration } from './integrations/shelly/index.js';
|
||||
import { SonosIntegration } from './integrations/sonos/index.js';
|
||||
import { WolfSmartsetIntegration } from './integrations/wolf_smartset/index.js';
|
||||
import { ZwaveJsIntegration } from './integrations/zwave_js/index.js';
|
||||
import { generatedHomeAssistantPortIntegrations } from './integrations/generated/index.js';
|
||||
import { IntegrationRegistry } from './core/index.js';
|
||||
|
||||
@@ -20,6 +21,7 @@ export const integrations = [
|
||||
new ShellyIntegration(),
|
||||
new SonosIntegration(),
|
||||
new WolfSmartsetIntegration(),
|
||||
new ZwaveJsIntegration(),
|
||||
];
|
||||
|
||||
export const createDefaultIntegrationRegistry = (): IntegrationRegistry => {
|
||||
|
||||
@@ -1451,7 +1451,6 @@ import { HomeAssistantZodiacIntegration } from '../zodiac/index.js';
|
||||
import { HomeAssistantZondergasIntegration } from '../zondergas/index.js';
|
||||
import { HomeAssistantZoneIntegration } from '../zone/index.js';
|
||||
import { HomeAssistantZoneminderIntegration } from '../zoneminder/index.js';
|
||||
import { HomeAssistantZwaveJsIntegration } from '../zwave_js/index.js';
|
||||
import { HomeAssistantZwaveMeIntegration } from '../zwave_me/index.js';
|
||||
|
||||
export const generatedHomeAssistantPortIntegrations: BaseIntegration[] = [];
|
||||
@@ -2905,15 +2904,15 @@ generatedHomeAssistantPortIntegrations.push(new HomeAssistantZodiacIntegration()
|
||||
generatedHomeAssistantPortIntegrations.push(new HomeAssistantZondergasIntegration());
|
||||
generatedHomeAssistantPortIntegrations.push(new HomeAssistantZoneIntegration());
|
||||
generatedHomeAssistantPortIntegrations.push(new HomeAssistantZoneminderIntegration());
|
||||
generatedHomeAssistantPortIntegrations.push(new HomeAssistantZwaveJsIntegration());
|
||||
generatedHomeAssistantPortIntegrations.push(new HomeAssistantZwaveMeIntegration());
|
||||
|
||||
export const generatedHomeAssistantPortCount = 1452;
|
||||
export const generatedHomeAssistantPortCount = 1451;
|
||||
export const handwrittenHomeAssistantPortDomains = [
|
||||
"cast",
|
||||
"hue",
|
||||
"mqtt",
|
||||
"roku",
|
||||
"shelly",
|
||||
"sonos"
|
||||
"sonos",
|
||||
"zwave_js"
|
||||
];
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support.
|
||||
@@ -1,2 +1,6 @@
|
||||
export * from './zwave_js.classes.integration.js';
|
||||
export * from './zwave_js.classes.client.js';
|
||||
export * from './zwave_js.classes.configflow.js';
|
||||
export * from './zwave_js.discovery.js';
|
||||
export * from './zwave_js.mapper.js';
|
||||
export * from './zwave_js.types.js';
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import type { IZwaveJsConfig, IZwaveJsServerCommand, IZwaveJsServerEvent, IZwaveJsSnapshot, IZwaveJsState, IZwaveJsVersion } from './zwave_js.types.js';
|
||||
import { ZwaveJsMapper } from './zwave_js.mapper.js';
|
||||
|
||||
type TEventHandler = (eventArg: IZwaveJsServerEvent) => void;
|
||||
|
||||
interface IPendingRequest {
|
||||
resolve(valueArg: unknown): void;
|
||||
reject(errorArg: Error): void;
|
||||
timer: ReturnType<typeof setTimeout>;
|
||||
}
|
||||
|
||||
export class ZwaveJsClient {
|
||||
private socket?: any;
|
||||
private started = false;
|
||||
private version?: IZwaveJsVersion;
|
||||
private state?: IZwaveJsState;
|
||||
private readonly events: IZwaveJsServerEvent[] = [];
|
||||
private readonly pendingRequests = new Map<string, IPendingRequest>();
|
||||
private readonly eventHandlers = new Set<TEventHandler>();
|
||||
|
||||
constructor(private readonly config: IZwaveJsConfig) {
|
||||
this.version = config.version;
|
||||
this.state = config.state || { controller: config.controller, nodes: config.nodes || [] };
|
||||
}
|
||||
|
||||
public async getSnapshot(): Promise<IZwaveJsSnapshot> {
|
||||
if (this.config.url && !this.started) {
|
||||
await this.start();
|
||||
}
|
||||
return {
|
||||
...ZwaveJsMapper.toSnapshot({ ...this.config, version: this.version, state: this.state }, Boolean(this.socket && this.socket.readyState === 1), this.events),
|
||||
url: this.config.url,
|
||||
};
|
||||
}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
if (this.started || !this.config.url) {
|
||||
this.started = true;
|
||||
return;
|
||||
}
|
||||
const WebSocketCtor = (globalThis as typeof globalThis & { WebSocket?: new (urlArg: string) => any }).WebSocket;
|
||||
if (!WebSocketCtor) {
|
||||
throw new Error('Global WebSocket is not available in this runtime.');
|
||||
}
|
||||
const socket = new WebSocketCtor(this.config.url);
|
||||
this.socket = socket;
|
||||
socket.addEventListener('message', (eventArg: { data: unknown }) => this.handleMessage(eventArg.data));
|
||||
socket.addEventListener('close', () => this.rejectAll(new Error('Z-Wave JS WebSocket closed.')));
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
socket.addEventListener('open', () => resolve(), { once: true });
|
||||
socket.addEventListener('error', () => reject(new Error(`Unable to connect to Z-Wave JS Server at ${this.config.url}.`)), { once: true });
|
||||
});
|
||||
await this.waitForVersion();
|
||||
await this.sendCommand({ command: 'initialize', schemaVersion: this.config.schemaVersion ?? this.version?.maxSchemaVersion ?? 0, additionalUserAgentComponents: { 'smarthome.exchange': '0.1.0' } }).catch(async () => {
|
||||
await this.sendCommand({ command: 'set_api_schema', schemaVersion: this.config.schemaVersion ?? this.version?.maxSchemaVersion ?? 0 });
|
||||
});
|
||||
const result = await this.sendCommand({ command: 'start_listening' });
|
||||
if (this.isRecord(result) && this.isRecord(result.state)) {
|
||||
this.state = result.state as IZwaveJsState;
|
||||
}
|
||||
this.started = true;
|
||||
}
|
||||
|
||||
public async sendCommand(commandArg: IZwaveJsServerCommand): Promise<unknown> {
|
||||
await this.ensureStarted();
|
||||
if (!this.socket || this.socket.readyState !== 1) {
|
||||
throw new Error('Z-Wave JS WebSocket is not connected.');
|
||||
}
|
||||
const messageId = `shx-${plugins.crypto.randomBytes(6).toString('hex')}`;
|
||||
const payload = { ...commandArg, messageId };
|
||||
const promise = new Promise<unknown>((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
this.pendingRequests.delete(messageId);
|
||||
reject(new Error(`Z-Wave JS command ${commandArg.command} timed out.`));
|
||||
}, 10000);
|
||||
this.pendingRequests.set(messageId, { resolve, reject, timer });
|
||||
});
|
||||
this.socket.send(JSON.stringify(payload));
|
||||
return promise;
|
||||
}
|
||||
|
||||
public onEvent(handlerArg: TEventHandler): () => void {
|
||||
this.eventHandlers.add(handlerArg);
|
||||
return () => this.eventHandlers.delete(handlerArg);
|
||||
}
|
||||
|
||||
public async destroy(): Promise<void> {
|
||||
this.rejectAll(new Error('Z-Wave JS client destroyed.'));
|
||||
if (this.socket?.readyState === 1) {
|
||||
this.socket.close();
|
||||
}
|
||||
this.socket = undefined;
|
||||
this.started = false;
|
||||
this.eventHandlers.clear();
|
||||
}
|
||||
|
||||
private async ensureStarted(): Promise<void> {
|
||||
if (!this.started) {
|
||||
await this.start();
|
||||
}
|
||||
}
|
||||
|
||||
private async waitForVersion(): Promise<void> {
|
||||
if (this.version) {
|
||||
return;
|
||||
}
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
private handleMessage(dataArg: unknown): void {
|
||||
const text = typeof dataArg === 'string' ? dataArg : dataArg instanceof Buffer ? dataArg.toString('utf8') : String(dataArg);
|
||||
const message = JSON.parse(text) as Record<string, unknown>;
|
||||
if (message.type === 'version') {
|
||||
this.version = message as IZwaveJsVersion;
|
||||
return;
|
||||
}
|
||||
if (message.type === 'result') {
|
||||
this.handleResult(message);
|
||||
return;
|
||||
}
|
||||
if (message.type === 'event' && this.isRecord(message.event)) {
|
||||
const event = message.event as IZwaveJsServerEvent;
|
||||
this.events.push(event);
|
||||
this.applyEvent(event);
|
||||
for (const handler of this.eventHandlers) {
|
||||
handler(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleResult(messageArg: Record<string, unknown>): void {
|
||||
const messageId = typeof messageArg.messageId === 'string' ? messageArg.messageId : undefined;
|
||||
if (!messageId) {
|
||||
return;
|
||||
}
|
||||
const pending = this.pendingRequests.get(messageId);
|
||||
if (!pending) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(pending.timer);
|
||||
this.pendingRequests.delete(messageId);
|
||||
if (messageArg.success === false) {
|
||||
pending.reject(new Error(`Z-Wave JS command failed: ${JSON.stringify(messageArg.error || messageArg)}`));
|
||||
return;
|
||||
}
|
||||
pending.resolve(messageArg.result);
|
||||
}
|
||||
|
||||
private applyEvent(eventArg: IZwaveJsServerEvent): void {
|
||||
if (eventArg.source !== 'node' || typeof eventArg.nodeId !== 'number') {
|
||||
return;
|
||||
}
|
||||
const nodes = this.state?.nodes || [];
|
||||
const node = nodes.find((nodeArg) => nodeArg.nodeId === eventArg.nodeId);
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
if (eventArg.event === 'value updated' && eventArg.valueId) {
|
||||
const value = ZwaveJsMapper.nodeValues(node).find((valueArg) => this.valueIdMatches(valueArg, eventArg.valueId!));
|
||||
if (value) {
|
||||
value.value = eventArg.newValue ?? eventArg.value;
|
||||
}
|
||||
}
|
||||
if (eventArg.event === 'node status' && typeof eventArg.status === 'string') {
|
||||
node.status = eventArg.status;
|
||||
}
|
||||
}
|
||||
|
||||
private valueIdMatches(valueArg: ReturnType<typeof ZwaveJsMapper.nodeValues>[number], valueIdArg: NonNullable<IZwaveJsServerEvent['valueId']>): boolean {
|
||||
return (valueArg.commandClass || 0) === valueIdArg.commandClass
|
||||
&& (valueArg.endpoint || 0) === (valueIdArg.endpoint || 0)
|
||||
&& String(valueArg.property ?? valueArg.propertyName ?? '') === String(valueIdArg.property)
|
||||
&& String(valueArg.propertyKey ?? '') === String(valueIdArg.propertyKey ?? '');
|
||||
}
|
||||
|
||||
private rejectAll(errorArg: Error): void {
|
||||
for (const [messageId, pending] of this.pendingRequests) {
|
||||
clearTimeout(pending.timer);
|
||||
pending.reject(errorArg);
|
||||
this.pendingRequests.delete(messageId);
|
||||
}
|
||||
}
|
||||
|
||||
private isRecord(valueArg: unknown): valueArg is Record<string, unknown> {
|
||||
return typeof valueArg === 'object' && valueArg !== null && !Array.isArray(valueArg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { IConfigFlow, IConfigFlowContext, IConfigFlowStep, IDiscoveryCandidate } from '../../core/types.js';
|
||||
import type { IZwaveJsConfig } from './zwave_js.types.js';
|
||||
|
||||
export class ZwaveJsConfigFlow implements IConfigFlow<IZwaveJsConfig> {
|
||||
public async start(candidateArg: IDiscoveryCandidate, contextArg: IConfigFlowContext): Promise<IConfigFlowStep<IZwaveJsConfig>> {
|
||||
void contextArg;
|
||||
return {
|
||||
kind: 'form',
|
||||
title: 'Connect Z-Wave JS',
|
||||
description: 'Configure the local Z-Wave JS Server WebSocket endpoint.',
|
||||
fields: [
|
||||
{ name: 'url', label: 'Server URL', type: 'text', required: true },
|
||||
{ name: 'schemaVersion', label: 'API schema version', type: 'number' },
|
||||
],
|
||||
submit: async (valuesArg) => ({
|
||||
kind: 'done',
|
||||
title: 'Z-Wave JS configured',
|
||||
config: {
|
||||
url: this.stringValue(valuesArg.url) || this.urlFromCandidate(candidateArg),
|
||||
schemaVersion: this.numberValue(valuesArg.schemaVersion),
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private urlFromCandidate(candidateArg: IDiscoveryCandidate): string {
|
||||
if (typeof candidateArg.metadata?.url === 'string') {
|
||||
return candidateArg.metadata.url;
|
||||
}
|
||||
return candidateArg.host ? `ws://${candidateArg.host}:${candidateArg.port || 3000}` : 'ws://localhost:3000';
|
||||
}
|
||||
|
||||
private stringValue(valueArg: unknown): string | undefined {
|
||||
return typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
|
||||
}
|
||||
|
||||
private numberValue(valueArg: unknown): number | undefined {
|
||||
return typeof valueArg === 'number' && Number.isFinite(valueArg) ? valueArg : undefined;
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,126 @@
|
||||
import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js';
|
||||
import type * as shxInterfaces from '@smarthome.exchange/interfaces';
|
||||
import { BaseIntegration } from '../../core/classes.baseintegration.js';
|
||||
import type { IIntegrationEntity, IIntegrationEvent, IIntegrationRuntime, IIntegrationSetupContext, IServiceCallRequest, IServiceCallResult } from '../../core/types.js';
|
||||
import { ZwaveJsClient } from './zwave_js.classes.client.js';
|
||||
import { ZwaveJsConfigFlow } from './zwave_js.classes.configflow.js';
|
||||
import { createZwaveJsDiscoveryDescriptor } from './zwave_js.discovery.js';
|
||||
import { ZwaveJsMapper } from './zwave_js.mapper.js';
|
||||
import type { IZwaveJsConfig, IZwaveJsServerCommand } from './zwave_js.types.js';
|
||||
|
||||
export class HomeAssistantZwaveJsIntegration extends DescriptorOnlyIntegration {
|
||||
constructor() {
|
||||
super({
|
||||
domain: "zwave_js",
|
||||
displayName: "Z-Wave",
|
||||
status: 'descriptor-only',
|
||||
metadata: {
|
||||
"source": "home-assistant/core",
|
||||
"upstreamPath": "homeassistant/components/zwave_js",
|
||||
"upstreamDomain": "zwave_js",
|
||||
"integrationType": "hub",
|
||||
"iotClass": "local_push",
|
||||
"requirements": [
|
||||
"zwave-js-server-python==0.70.0"
|
||||
],
|
||||
"dependencies": [
|
||||
"http",
|
||||
"repairs",
|
||||
"usb",
|
||||
"websocket_api"
|
||||
],
|
||||
"afterDependencies": [
|
||||
"hassio"
|
||||
],
|
||||
"codeowners": [
|
||||
"@home-assistant/z-wave"
|
||||
]
|
||||
},
|
||||
export class ZwaveJsIntegration extends BaseIntegration<IZwaveJsConfig> {
|
||||
public readonly domain = 'zwave_js';
|
||||
public readonly displayName = 'Z-Wave';
|
||||
public readonly status = 'control-runtime' as const;
|
||||
public readonly discoveryDescriptor = createZwaveJsDiscoveryDescriptor();
|
||||
public readonly configFlow = new ZwaveJsConfigFlow();
|
||||
public readonly metadata = {
|
||||
source: 'home-assistant/core',
|
||||
upstreamPath: 'homeassistant/components/zwave_js',
|
||||
upstreamDomain: 'zwave_js',
|
||||
integrationType: 'hub',
|
||||
iotClass: 'local_push',
|
||||
qualityScale: 'unknown',
|
||||
};
|
||||
|
||||
public async setup(configArg: IZwaveJsConfig, contextArg: IIntegrationSetupContext): Promise<IIntegrationRuntime> {
|
||||
void contextArg;
|
||||
return new ZwaveJsRuntime(new ZwaveJsClient(configArg));
|
||||
}
|
||||
|
||||
public async destroy(): Promise<void> {}
|
||||
}
|
||||
|
||||
export class HomeAssistantZwaveJsIntegration extends ZwaveJsIntegration {}
|
||||
|
||||
class ZwaveJsRuntime implements IIntegrationRuntime {
|
||||
public domain = 'zwave_js';
|
||||
|
||||
constructor(private readonly client: ZwaveJsClient) {}
|
||||
|
||||
public async devices(): Promise<shxInterfaces.data.IDeviceDefinition[]> {
|
||||
return ZwaveJsMapper.toDevices(await this.client.getSnapshot());
|
||||
}
|
||||
|
||||
public async entities(): Promise<IIntegrationEntity[]> {
|
||||
return ZwaveJsMapper.toEntities(await this.client.getSnapshot());
|
||||
}
|
||||
|
||||
public async subscribe(handlerArg: (eventArg: IIntegrationEvent) => void): Promise<() => Promise<void>> {
|
||||
const unsubscribe = this.client.onEvent((eventArg) => {
|
||||
handlerArg({
|
||||
type: eventArg.event === 'value updated' ? 'state_changed' : 'device_added',
|
||||
integrationDomain: 'zwave_js',
|
||||
deviceId: typeof eventArg.nodeId === 'number' ? `zwave_js.node.${eventArg.nodeId}` : undefined,
|
||||
data: eventArg,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
});
|
||||
await this.client.getSnapshot();
|
||||
return async () => unsubscribe();
|
||||
}
|
||||
|
||||
public async callService(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
|
||||
if (requestArg.domain === 'zwave_js') {
|
||||
const command = this.commandFromService(requestArg);
|
||||
if (!command) {
|
||||
return { success: false, error: `Unsupported Z-Wave JS service: ${requestArg.service}` };
|
||||
}
|
||||
const data = await this.client.sendCommand(command);
|
||||
return { success: true, data };
|
||||
}
|
||||
|
||||
const command = ZwaveJsMapper.commandForService(await this.client.getSnapshot(), requestArg);
|
||||
if (!command) {
|
||||
return { success: false, error: `Z-Wave JS entity service ${requestArg.domain}.${requestArg.service} has no writable value mapping.` };
|
||||
}
|
||||
const data = await this.client.sendCommand(command);
|
||||
return { success: true, data };
|
||||
}
|
||||
|
||||
public async destroy(): Promise<void> {
|
||||
await this.client.destroy();
|
||||
}
|
||||
|
||||
private commandFromService(requestArg: IServiceCallRequest): IZwaveJsServerCommand | undefined {
|
||||
if (requestArg.service === 'ping') {
|
||||
const nodeId = this.numberValue(requestArg.data?.node_id ?? requestArg.data?.nodeId);
|
||||
return nodeId ? { command: 'node.ping', nodeId } : undefined;
|
||||
}
|
||||
if (requestArg.service === 'refresh_value') {
|
||||
const nodeId = this.numberValue(requestArg.data?.node_id ?? requestArg.data?.nodeId);
|
||||
const valueId = requestArg.data?.value_id ?? requestArg.data?.valueId;
|
||||
return nodeId && this.isRecord(valueId) ? { command: 'node.poll_value', nodeId, valueId } : undefined;
|
||||
}
|
||||
if (requestArg.service === 'set_value') {
|
||||
const nodeId = this.numberValue(requestArg.data?.node_id ?? requestArg.data?.nodeId);
|
||||
const valueId = requestArg.data?.value_id ?? requestArg.data?.valueId;
|
||||
return nodeId && this.isRecord(valueId) ? { command: 'node.set_value', nodeId, valueId, value: requestArg.data?.value } : undefined;
|
||||
}
|
||||
if (requestArg.service === 'set_config_parameter') {
|
||||
const nodeId = this.numberValue(requestArg.data?.node_id ?? requestArg.data?.nodeId);
|
||||
const parameter = this.numberValue(requestArg.data?.parameter);
|
||||
return nodeId && parameter ? { command: 'node.set_raw_config_parameter_value', nodeId, parameter, bitMask: requestArg.data?.bitmask, value: requestArg.data?.value, valueSize: requestArg.data?.value_size, valueFormat: requestArg.data?.value_format } : undefined;
|
||||
}
|
||||
if (requestArg.service === 'begin_inclusion') {
|
||||
return { command: 'controller.begin_inclusion', strategy: requestArg.data?.strategy || 0 };
|
||||
}
|
||||
if (requestArg.service === 'stop_inclusion') {
|
||||
return { command: 'controller.stop_inclusion' };
|
||||
}
|
||||
if (requestArg.service === 'invoke_cc_api') {
|
||||
const nodeId = this.numberValue(requestArg.data?.node_id ?? requestArg.data?.nodeId);
|
||||
const commandClass = this.numberValue(requestArg.data?.command_class ?? requestArg.data?.commandClass);
|
||||
const methodName = requestArg.data?.method_name ?? requestArg.data?.methodName;
|
||||
return nodeId && commandClass && typeof methodName === 'string' ? { command: 'endpoint.invoke_cc_api', nodeId, endpoint: requestArg.data?.endpoint, commandClass, methodName, args: Array.isArray(requestArg.data?.parameters) ? requestArg.data.parameters : [] } : undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private numberValue(valueArg: unknown): number | undefined {
|
||||
return typeof valueArg === 'number' && Number.isFinite(valueArg) ? valueArg : undefined;
|
||||
}
|
||||
|
||||
private isRecord(valueArg: unknown): valueArg is Record<string, unknown> {
|
||||
return typeof valueArg === 'object' && valueArg !== null && !Array.isArray(valueArg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
|
||||
import type { IDiscoveryCandidate, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
|
||||
import type { IZwaveJsManualEntry, IZwaveJsMdnsRecord, IZwaveJsUsbRecord } from './zwave_js.types.js';
|
||||
|
||||
const knownUsbIds = new Set(['0658:0200', '10c4:8a2a', '303a:4001', '10c4:ea60']);
|
||||
|
||||
export class ZwaveJsMdnsMatcher implements IDiscoveryMatcher<IZwaveJsMdnsRecord> {
|
||||
public id = 'zwave-js-mdns-match';
|
||||
public source = 'mdns' as const;
|
||||
public description = 'Recognize Z-Wave JS Server mDNS advertisements.';
|
||||
|
||||
public async matches(recordArg: IZwaveJsMdnsRecord): Promise<IDiscoveryMatch> {
|
||||
const type = (recordArg.type || '').toLowerCase().replace(/\.$/, '');
|
||||
const matched = type === '_zwave-js-server._tcp.local' || Boolean(recordArg.name?.toLowerCase().includes('zwave-js'));
|
||||
if (!matched) {
|
||||
return { matched: false, confidence: 'low', reason: 'mDNS record is not a Z-Wave JS Server advertisement.' };
|
||||
}
|
||||
return {
|
||||
matched: true,
|
||||
confidence: recordArg.host ? 'certain' : 'high',
|
||||
reason: 'mDNS record matches Z-Wave JS Server metadata.',
|
||||
normalizedDeviceId: recordArg.name,
|
||||
candidate: {
|
||||
source: 'mdns',
|
||||
integrationDomain: 'zwave_js',
|
||||
id: recordArg.name,
|
||||
host: recordArg.host,
|
||||
port: recordArg.port || 3000,
|
||||
name: 'Z-Wave JS Server',
|
||||
manufacturer: 'Z-Wave JS',
|
||||
model: 'WebSocket Server',
|
||||
metadata: { mdnsName: recordArg.name, mdnsType: recordArg.type, txt: recordArg.txt, url: recordArg.host ? `ws://${recordArg.host}:${recordArg.port || 3000}` : undefined },
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class ZwaveJsUsbMatcher implements IDiscoveryMatcher<IZwaveJsUsbRecord> {
|
||||
public id = 'zwave-js-usb-match';
|
||||
public source = 'usb' as const;
|
||||
public description = 'Recognize known Z-Wave USB adapters used by Z-Wave JS.';
|
||||
|
||||
public async matches(recordArg: IZwaveJsUsbRecord): Promise<IDiscoveryMatch> {
|
||||
const usbId = `${(recordArg.vid || '').toLowerCase()}:${(recordArg.pid || '').toLowerCase()}`;
|
||||
const description = `${recordArg.manufacturer || ''} ${recordArg.description || ''}`.toLowerCase();
|
||||
const matched = knownUsbIds.has(usbId) || description.includes('z-wave') || description.includes('zwave') || description.includes('zwa-2');
|
||||
if (!matched) {
|
||||
return { matched: false, confidence: 'low', reason: 'USB device is not a known Z-Wave adapter.' };
|
||||
}
|
||||
return {
|
||||
matched: true,
|
||||
confidence: knownUsbIds.has(usbId) ? 'certain' : 'high',
|
||||
reason: 'USB record matches known Z-Wave adapter metadata.',
|
||||
normalizedDeviceId: recordArg.serialNumber || recordArg.path || usbId,
|
||||
candidate: {
|
||||
source: 'usb',
|
||||
integrationDomain: 'zwave_js',
|
||||
id: recordArg.serialNumber || recordArg.path || usbId,
|
||||
name: recordArg.description || 'Z-Wave adapter',
|
||||
manufacturer: recordArg.manufacturer || 'Z-Wave',
|
||||
model: recordArg.description,
|
||||
serialNumber: recordArg.serialNumber,
|
||||
metadata: { vid: recordArg.vid, pid: recordArg.pid, path: recordArg.path },
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class ZwaveJsManualMatcher implements IDiscoveryMatcher<IZwaveJsManualEntry> {
|
||||
public id = 'zwave-js-manual-match';
|
||||
public source = 'manual' as const;
|
||||
public description = 'Recognize manual Z-Wave JS setup entries.';
|
||||
|
||||
public async matches(inputArg: IZwaveJsManualEntry): Promise<IDiscoveryMatch> {
|
||||
const model = inputArg.model?.toLowerCase() || '';
|
||||
const url = inputArg.url || (inputArg.host ? `ws://${inputArg.host}:${inputArg.port || 3000}` : undefined);
|
||||
const matched = Boolean(url || inputArg.usbPath || inputArg.metadata?.zwaveJs || model.includes('z-wave') || model.includes('zwave'));
|
||||
if (!matched) {
|
||||
return { matched: false, confidence: 'low', reason: 'Manual entry does not contain Z-Wave JS setup hints.' };
|
||||
}
|
||||
return {
|
||||
matched: true,
|
||||
confidence: url ? 'high' : 'medium',
|
||||
reason: 'Manual entry can start Z-Wave JS setup.',
|
||||
normalizedDeviceId: inputArg.id || url || inputArg.usbPath,
|
||||
candidate: {
|
||||
source: 'manual',
|
||||
integrationDomain: 'zwave_js',
|
||||
id: inputArg.id || url || inputArg.usbPath,
|
||||
host: inputArg.host,
|
||||
port: inputArg.port || 3000,
|
||||
name: inputArg.name || 'Z-Wave JS',
|
||||
manufacturer: 'Z-Wave JS',
|
||||
model: inputArg.model || 'WebSocket Server',
|
||||
metadata: { ...inputArg.metadata, url, usbPath: inputArg.usbPath },
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class ZwaveJsCandidateValidator implements IDiscoveryValidator {
|
||||
public id = 'zwave-js-candidate-validator';
|
||||
public description = 'Validate Z-Wave JS candidate metadata.';
|
||||
|
||||
public async validate(candidateArg: IDiscoveryCandidate): Promise<IDiscoveryMatch> {
|
||||
const model = candidateArg.model?.toLowerCase() || '';
|
||||
const manufacturer = candidateArg.manufacturer?.toLowerCase() || '';
|
||||
const matched = candidateArg.integrationDomain === 'zwave_js' || model.includes('z-wave') || model.includes('zwave') || manufacturer.includes('z-wave') || Boolean(candidateArg.metadata?.zwaveJs);
|
||||
return {
|
||||
matched,
|
||||
confidence: matched && (candidateArg.host || candidateArg.metadata?.url || candidateArg.source === 'usb') ? 'high' : matched ? 'medium' : 'low',
|
||||
reason: matched ? 'Candidate has Z-Wave JS metadata.' : 'Candidate is not Z-Wave JS.',
|
||||
candidate: matched ? candidateArg : undefined,
|
||||
normalizedDeviceId: candidateArg.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const createZwaveJsDiscoveryDescriptor = (): DiscoveryDescriptor => {
|
||||
return new DiscoveryDescriptor({ integrationDomain: 'zwave_js', displayName: 'Z-Wave' })
|
||||
.addMatcher(new ZwaveJsMdnsMatcher())
|
||||
.addMatcher(new ZwaveJsUsbMatcher())
|
||||
.addMatcher(new ZwaveJsManualMatcher())
|
||||
.addValidator(new ZwaveJsCandidateValidator());
|
||||
};
|
||||
@@ -0,0 +1,321 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import type { IIntegrationEntity, IServiceCallRequest, TEntityPlatform } from '../../core/types.js';
|
||||
import type { IZwaveJsConfig, IZwaveJsNode, IZwaveJsServerCommand, IZwaveJsSnapshot, IZwaveJsValue, IZwaveJsValueId } from './zwave_js.types.js';
|
||||
|
||||
export class ZwaveJsMapper {
|
||||
public static toSnapshot(configArg: IZwaveJsConfig, connectedArg = false, eventsArg: IZwaveJsSnapshot['events'] = []): IZwaveJsSnapshot {
|
||||
return {
|
||||
version: configArg.version,
|
||||
controller: configArg.controller || configArg.state?.controller,
|
||||
nodes: configArg.nodes || configArg.state?.nodes || [],
|
||||
events: eventsArg,
|
||||
connected: connectedArg,
|
||||
url: configArg.url,
|
||||
};
|
||||
}
|
||||
|
||||
public static toDevices(snapshotArg: IZwaveJsSnapshot): plugins.shxInterfaces.data.IDeviceDefinition[] {
|
||||
const updatedAt = new Date().toISOString();
|
||||
const devices: plugins.shxInterfaces.data.IDeviceDefinition[] = [];
|
||||
devices.push({
|
||||
id: this.controllerDeviceId(snapshotArg),
|
||||
integrationDomain: 'zwave_js',
|
||||
name: 'Z-Wave controller',
|
||||
protocol: 'zwave',
|
||||
manufacturer: 'Z-Wave JS',
|
||||
model: 'Controller',
|
||||
online: snapshotArg.connected || Boolean(snapshotArg.nodes.length),
|
||||
features: [
|
||||
{ id: 'connection', capability: 'sensor', name: 'Connection', readable: true, writable: false },
|
||||
{ id: 'node_count', capability: 'sensor', name: 'Node count', readable: true, writable: false },
|
||||
],
|
||||
state: [
|
||||
{ featureId: 'connection', value: snapshotArg.connected ? 'connected' : 'configured', updatedAt },
|
||||
{ featureId: 'node_count', value: snapshotArg.nodes.length, updatedAt },
|
||||
],
|
||||
metadata: {
|
||||
homeId: snapshotArg.controller?.homeId ?? snapshotArg.version?.homeId,
|
||||
serverVersion: snapshotArg.version?.serverVersion,
|
||||
driverVersion: snapshotArg.version?.driverVersion,
|
||||
url: snapshotArg.url,
|
||||
},
|
||||
});
|
||||
|
||||
for (const node of snapshotArg.nodes) {
|
||||
const features: plugins.shxInterfaces.data.IDeviceFeature[] = [
|
||||
{ id: 'status', capability: 'sensor', name: 'Node status', readable: true, writable: false },
|
||||
];
|
||||
const state: plugins.shxInterfaces.data.IDeviceState[] = [
|
||||
{ featureId: 'status', value: node.status || (node.ready ? 'ready' : 'unknown'), updatedAt },
|
||||
];
|
||||
for (const value of this.nodeValues(node)) {
|
||||
const feature = this.featureForValue(value);
|
||||
features.push(feature);
|
||||
state.push({ featureId: feature.id, value: this.stateValue(value.value), updatedAt });
|
||||
}
|
||||
devices.push({
|
||||
id: this.nodeDeviceId(node),
|
||||
integrationDomain: 'zwave_js',
|
||||
name: this.nodeName(node),
|
||||
room: node.location,
|
||||
protocol: 'zwave',
|
||||
manufacturer: node.manufacturer,
|
||||
model: node.productLabel || node.productDescription,
|
||||
online: node.status ? !['dead', 'asleep'].includes(node.status.toLowerCase()) : node.ready !== false,
|
||||
features,
|
||||
state,
|
||||
metadata: {
|
||||
nodeId: node.nodeId,
|
||||
manufacturerId: node.manufacturerId,
|
||||
productId: node.productId,
|
||||
productType: node.productType,
|
||||
firmwareVersion: node.firmwareVersion,
|
||||
isControllerNode: node.isControllerNode,
|
||||
},
|
||||
});
|
||||
}
|
||||
return devices;
|
||||
}
|
||||
|
||||
public static toEntities(snapshotArg: IZwaveJsSnapshot): IIntegrationEntity[] {
|
||||
const entities: IIntegrationEntity[] = [];
|
||||
for (const node of snapshotArg.nodes) {
|
||||
const deviceId = this.nodeDeviceId(node);
|
||||
for (const value of this.nodeValues(node)) {
|
||||
const platform = this.platformForValue(value);
|
||||
entities.push({
|
||||
id: `${platform}.${this.slug(`${this.nodeName(node)} ${this.valueLabel(value)}`)}`,
|
||||
uniqueId: `zwave_js_${this.slug(`${node.nodeId}_${this.valueIdKey(value)}`)}`,
|
||||
integrationDomain: 'zwave_js',
|
||||
deviceId,
|
||||
platform,
|
||||
name: this.valueLabel(value),
|
||||
state: this.entityState(value, platform),
|
||||
attributes: {
|
||||
nodeId: node.nodeId,
|
||||
valueId: this.valueId(value),
|
||||
commandClass: value.commandClass,
|
||||
commandClassName: value.commandClassName,
|
||||
endpoint: value.endpoint,
|
||||
property: value.property,
|
||||
propertyKey: value.propertyKey,
|
||||
unit: value.metadata?.unit,
|
||||
zwaveType: value.metadata?.type,
|
||||
states: value.metadata?.states,
|
||||
},
|
||||
available: node.ready !== false && node.status !== 'dead',
|
||||
});
|
||||
}
|
||||
}
|
||||
return entities;
|
||||
}
|
||||
|
||||
public static commandForService(snapshotArg: IZwaveJsSnapshot, requestArg: IServiceCallRequest): IZwaveJsServerCommand | undefined {
|
||||
const target = this.findTargetValue(snapshotArg, requestArg);
|
||||
if (!target) {
|
||||
return undefined;
|
||||
}
|
||||
let value: unknown;
|
||||
if (requestArg.service === 'turn_on') {
|
||||
value = this.onValue(target.value);
|
||||
} else if (requestArg.service === 'turn_off') {
|
||||
value = this.offValue(target.value);
|
||||
} else if (requestArg.service === 'set_value') {
|
||||
value = requestArg.data?.value;
|
||||
} else if (requestArg.service === 'set_position') {
|
||||
value = requestArg.data?.position;
|
||||
} else if (requestArg.service === 'select_option') {
|
||||
value = requestArg.data?.option;
|
||||
} else if (requestArg.service === 'set_percentage') {
|
||||
value = requestArg.data?.percentage;
|
||||
} else if (requestArg.service === 'open_cover') {
|
||||
value = 99;
|
||||
} else if (requestArg.service === 'close_cover') {
|
||||
value = 0;
|
||||
} else if (requestArg.service === 'press') {
|
||||
value = true;
|
||||
}
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
command: 'node.set_value',
|
||||
nodeId: target.node.nodeId,
|
||||
valueId: this.valueId(target.value),
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
public static valueId(valueArg: IZwaveJsValue): IZwaveJsValueId {
|
||||
return {
|
||||
commandClass: valueArg.commandClass || 0,
|
||||
endpoint: valueArg.endpoint,
|
||||
property: valueArg.property ?? valueArg.propertyName ?? 'value',
|
||||
propertyKey: valueArg.propertyKey,
|
||||
};
|
||||
}
|
||||
|
||||
public static nodeValues(nodeArg: IZwaveJsNode): IZwaveJsValue[] {
|
||||
if (Array.isArray(nodeArg.values)) {
|
||||
return nodeArg.values.map((valueArg) => ({ ...valueArg, nodeId: nodeArg.nodeId }));
|
||||
}
|
||||
if (nodeArg.values && typeof nodeArg.values === 'object') {
|
||||
return Object.entries(nodeArg.values).map(([id, value]) => ({ ...value, id, nodeId: nodeArg.nodeId }));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private static findTargetValue(snapshotArg: IZwaveJsSnapshot, requestArg: IServiceCallRequest): { node: IZwaveJsNode; value: IZwaveJsValue } | undefined {
|
||||
const entities = this.toEntities(snapshotArg);
|
||||
if (requestArg.target.entityId) {
|
||||
const entity = entities.find((entityArg) => entityArg.id === requestArg.target.entityId || entityArg.uniqueId === requestArg.target.entityId);
|
||||
if (!entity) {
|
||||
return undefined;
|
||||
}
|
||||
return this.nodeValueByUniqueId(snapshotArg, entity.uniqueId);
|
||||
}
|
||||
if (requestArg.target.deviceId) {
|
||||
const node = snapshotArg.nodes.find((nodeArg) => this.nodeDeviceId(nodeArg) === requestArg.target.deviceId);
|
||||
const value = node ? this.nodeValues(node).find((valueArg) => this.valueWritable(valueArg)) : undefined;
|
||||
return node && value ? { node, value } : undefined;
|
||||
}
|
||||
for (const node of snapshotArg.nodes) {
|
||||
const value = this.nodeValues(node).find((valueArg) => this.valueWritable(valueArg));
|
||||
if (value) {
|
||||
return { node, value };
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private static nodeValueByUniqueId(snapshotArg: IZwaveJsSnapshot, uniqueIdArg: string): { node: IZwaveJsNode; value: IZwaveJsValue } | undefined {
|
||||
for (const node of snapshotArg.nodes) {
|
||||
for (const value of this.nodeValues(node)) {
|
||||
if (`zwave_js_${this.slug(`${node.nodeId}_${this.valueIdKey(value)}`)}` === uniqueIdArg) {
|
||||
return { node, value };
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private static featureForValue(valueArg: IZwaveJsValue): plugins.shxInterfaces.data.IDeviceFeature {
|
||||
const platform = this.platformForValue(valueArg);
|
||||
return {
|
||||
id: this.slug(this.valueIdKey(valueArg)),
|
||||
capability: this.capabilityForPlatform(platform),
|
||||
name: this.valueLabel(valueArg),
|
||||
readable: valueArg.metadata?.readable !== false,
|
||||
writable: this.valueWritable(valueArg),
|
||||
unit: valueArg.metadata?.unit,
|
||||
};
|
||||
}
|
||||
|
||||
private static platformForValue(valueArg: IZwaveJsValue): TEntityPlatform {
|
||||
const label = this.valueLabel(valueArg).toLowerCase();
|
||||
const cc = (valueArg.commandClassName || '').toLowerCase();
|
||||
const type = valueArg.metadata?.type;
|
||||
const writable = this.valueWritable(valueArg);
|
||||
if (cc.includes('switch binary') || (type === 'boolean' && writable)) {
|
||||
return 'switch';
|
||||
}
|
||||
if (cc.includes('switch multilevel') && writable) {
|
||||
return 'light';
|
||||
}
|
||||
if (cc.includes('window covering')) {
|
||||
return 'cover';
|
||||
}
|
||||
if (cc.includes('thermostat')) {
|
||||
return 'climate';
|
||||
}
|
||||
if (valueArg.metadata?.states && writable) {
|
||||
return 'select';
|
||||
}
|
||||
if (type === 'number' && writable) {
|
||||
return 'number';
|
||||
}
|
||||
if (type === 'boolean') {
|
||||
return 'binary_sensor';
|
||||
}
|
||||
if (label.includes('firmware')) {
|
||||
return 'update';
|
||||
}
|
||||
return 'sensor';
|
||||
}
|
||||
|
||||
private static capabilityForPlatform(platformArg: TEntityPlatform): plugins.shxInterfaces.data.TDeviceCapability {
|
||||
if (platformArg === 'light') {
|
||||
return 'light';
|
||||
}
|
||||
if (platformArg === 'switch' || platformArg === 'button' || platformArg === 'number' || platformArg === 'select' || platformArg === 'text') {
|
||||
return 'switch';
|
||||
}
|
||||
if (platformArg === 'cover') {
|
||||
return 'cover';
|
||||
}
|
||||
if (platformArg === 'climate') {
|
||||
return 'climate';
|
||||
}
|
||||
if (platformArg === 'fan') {
|
||||
return 'fan';
|
||||
}
|
||||
return 'sensor';
|
||||
}
|
||||
|
||||
private static entityState(valueArg: IZwaveJsValue, platformArg: TEntityPlatform): unknown {
|
||||
if (platformArg === 'switch' || platformArg === 'light') {
|
||||
return valueArg.value ? 'on' : 'off';
|
||||
}
|
||||
return valueArg.value ?? 'unknown';
|
||||
}
|
||||
|
||||
private static valueWritable(valueArg: IZwaveJsValue): boolean {
|
||||
return valueArg.metadata?.writeable === true || valueArg.metadata?.writeable === undefined && valueArg.commandClassName?.toLowerCase().includes('switch') === true;
|
||||
}
|
||||
|
||||
private static valueLabel(valueArg: IZwaveJsValue): string {
|
||||
return valueArg.metadata?.label || valueArg.propertyName || String(valueArg.property ?? valueArg.id ?? 'Z-Wave value');
|
||||
}
|
||||
|
||||
private static valueIdKey(valueArg: IZwaveJsValue): string {
|
||||
return [valueArg.commandClass, valueArg.endpoint || 0, valueArg.property ?? valueArg.propertyName ?? 'value', valueArg.propertyKey ?? ''].join('_');
|
||||
}
|
||||
|
||||
private static onValue(currentArg: unknown): unknown {
|
||||
return typeof currentArg === 'number' ? 99 : true;
|
||||
}
|
||||
|
||||
private static offValue(currentArg: unknown): unknown {
|
||||
return typeof currentArg === 'number' ? 0 : false;
|
||||
}
|
||||
|
||||
private static stateValue(valueArg: unknown): plugins.shxInterfaces.data.TDeviceStateValue {
|
||||
if (Array.isArray(valueArg)) {
|
||||
return JSON.stringify(valueArg);
|
||||
}
|
||||
if (typeof valueArg === 'string' || typeof valueArg === 'number' || typeof valueArg === 'boolean' || valueArg === null || this.isRecord(valueArg)) {
|
||||
return valueArg;
|
||||
}
|
||||
return valueArg === undefined ? null : String(valueArg);
|
||||
}
|
||||
|
||||
private static nodeDeviceId(nodeArg: IZwaveJsNode): string {
|
||||
return `zwave_js.node.${nodeArg.nodeId}`;
|
||||
}
|
||||
|
||||
private static controllerDeviceId(snapshotArg: IZwaveJsSnapshot): string {
|
||||
return `zwave_js.controller.${snapshotArg.controller?.homeId ?? snapshotArg.version?.homeId ?? 'network'}`;
|
||||
}
|
||||
|
||||
private static nodeName(nodeArg: IZwaveJsNode): string {
|
||||
return nodeArg.name || nodeArg.productLabel || nodeArg.productDescription || `Z-Wave node ${nodeArg.nodeId}`;
|
||||
}
|
||||
|
||||
private static isRecord(valueArg: unknown): valueArg is Record<string, unknown> {
|
||||
return typeof valueArg === 'object' && valueArg !== null && !Array.isArray(valueArg);
|
||||
}
|
||||
|
||||
private static slug(valueArg: string): string {
|
||||
return valueArg.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '') || 'zwave_js';
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,138 @@
|
||||
export interface IHomeAssistantZwaveJsConfig {
|
||||
// TODO: replace with the TypeScript-native config for zwave_js.
|
||||
export interface IZwaveJsConfig {
|
||||
url?: string;
|
||||
schemaVersion?: number;
|
||||
version?: IZwaveJsVersion;
|
||||
state?: IZwaveJsState;
|
||||
nodes?: IZwaveJsNode[];
|
||||
controller?: IZwaveJsController;
|
||||
}
|
||||
|
||||
export interface IZwaveJsVersion {
|
||||
type?: 'version';
|
||||
driverVersion?: string;
|
||||
serverVersion?: string;
|
||||
homeId?: number;
|
||||
minSchemaVersion?: number;
|
||||
maxSchemaVersion?: number;
|
||||
}
|
||||
|
||||
export interface IZwaveJsState {
|
||||
driver?: Record<string, unknown>;
|
||||
controller?: IZwaveJsController;
|
||||
nodes?: IZwaveJsNode[];
|
||||
}
|
||||
|
||||
export interface IZwaveJsController {
|
||||
homeId?: number;
|
||||
ownNodeId?: number;
|
||||
status?: string;
|
||||
isUsingHomeIdFromOtherNetwork?: boolean;
|
||||
statistics?: Record<string, unknown>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface IZwaveJsNode {
|
||||
nodeId: number;
|
||||
id?: number;
|
||||
name?: string;
|
||||
location?: string;
|
||||
status?: string;
|
||||
ready?: boolean;
|
||||
isControllerNode?: boolean;
|
||||
manufacturer?: string;
|
||||
manufacturerId?: number;
|
||||
productLabel?: string;
|
||||
productDescription?: string;
|
||||
productId?: number;
|
||||
productType?: number;
|
||||
firmwareVersion?: string;
|
||||
deviceConfig?: Record<string, unknown>;
|
||||
values?: Record<string, IZwaveJsValue> | IZwaveJsValue[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface IZwaveJsValueId {
|
||||
commandClass: number;
|
||||
endpoint?: number;
|
||||
property: string | number;
|
||||
propertyKey?: string | number;
|
||||
}
|
||||
|
||||
export interface IZwaveJsValueMetadata {
|
||||
label?: string;
|
||||
type?: string;
|
||||
readable?: boolean;
|
||||
writeable?: boolean;
|
||||
unit?: string;
|
||||
states?: Record<string, string>;
|
||||
min?: number;
|
||||
max?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface IZwaveJsValue {
|
||||
id?: string;
|
||||
nodeId?: number;
|
||||
commandClass?: number;
|
||||
commandClassName?: string;
|
||||
endpoint?: number;
|
||||
property?: string | number;
|
||||
propertyName?: string;
|
||||
propertyKey?: string | number;
|
||||
propertyKeyName?: string;
|
||||
metadata?: IZwaveJsValueMetadata;
|
||||
value?: unknown;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface IZwaveJsSnapshot {
|
||||
version?: IZwaveJsVersion;
|
||||
controller?: IZwaveJsController;
|
||||
nodes: IZwaveJsNode[];
|
||||
events: IZwaveJsServerEvent[];
|
||||
connected: boolean;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export interface IZwaveJsServerCommand {
|
||||
command: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface IZwaveJsServerEvent {
|
||||
source?: string;
|
||||
event?: string;
|
||||
nodeId?: number;
|
||||
valueId?: IZwaveJsValueId;
|
||||
value?: unknown;
|
||||
newValue?: unknown;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface IZwaveJsMdnsRecord {
|
||||
type?: string;
|
||||
name?: string;
|
||||
host?: string;
|
||||
port?: number;
|
||||
txt?: Record<string, string | undefined>;
|
||||
}
|
||||
|
||||
export interface IZwaveJsUsbRecord {
|
||||
vid?: string;
|
||||
pid?: string;
|
||||
manufacturer?: string;
|
||||
description?: string;
|
||||
path?: string;
|
||||
serialNumber?: string;
|
||||
}
|
||||
|
||||
export interface IZwaveJsManualEntry {
|
||||
url?: string;
|
||||
host?: string;
|
||||
port?: number;
|
||||
usbPath?: string;
|
||||
id?: string;
|
||||
name?: string;
|
||||
model?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user