inventoresed
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
685 lines (596 loc) • 18.8 kB
text/typescript
import {
createSimpleReflectionDecorator,
MessageOrCCLogEntry,
MessagePriority,
MessageRecord,
parseBitMask,
RFRegion,
validatePayload,
ZWaveError,
ZWaveErrorCodes,
} from "@zwave-js/core";
import type { ZWaveHost } from "@zwave-js/host";
import type {
DeserializingMessageConstructor,
SuccessIndicator,
} from "@zwave-js/serial";
import {
expectedResponse,
FunctionType,
gotDeserializationOptions,
Message,
MessageBaseOptions,
MessageDeserializationOptions,
MessageOptions,
MessageType,
messageTypes,
priority,
} from "@zwave-js/serial";
import { getEnumMemberName } from "@zwave-js/shared";
import { NodeIDType } from "../_Types";
export enum SerialAPISetupCommand {
Unsupported = 0x00,
GetSupportedCommands = 0x01,
SetTxStatusReport = 0x02,
SetPowerlevel = 0x04,
GetPowerlevel = 0x08,
GetMaximumPayloadSize = 0x10,
GetLRMaximumPayloadSize = 0x11,
GetRFRegion = 0x20,
SetRFRegion = 0x40,
SetNodeIDType = 0x80,
}
// We need to define the decorators for Requests and Responses separately
const {
decorator: subCommandRequest,
// lookupConstructor: getSubCommandRequestConstructor,
lookupValue: getSubCommandForRequest,
} = createSimpleReflectionDecorator<
SerialAPISetupRequest,
[command: SerialAPISetupCommand],
DeserializingMessageConstructor<SerialAPISetupRequest>
>({
name: "subCommandRequest",
});
const {
decorator: subCommandResponse,
lookupConstructor: getSubCommandResponseConstructor,
} = createSimpleReflectionDecorator<
SerialAPISetupResponse,
[command: SerialAPISetupCommand],
DeserializingMessageConstructor<SerialAPISetupResponse>
>({
name: "subCommandResponse",
});
function testResponseForSerialAPISetupRequest(
sent: Message,
received: Message,
) {
if (!(received instanceof SerialAPISetupResponse)) return false;
return (sent as SerialAPISetupRequest).command === received.command;
}
export class SerialAPISetupRequest extends Message {
public constructor(host: ZWaveHost, options: MessageOptions = {}) {
super(host, options);
if (gotDeserializationOptions(options)) {
throw new ZWaveError(
`${this.constructor.name}: deserialization not implemented`,
ZWaveErrorCodes.Deserialization_NotImplemented,
);
} else {
this.command = getSubCommandForRequest(this)!;
}
}
public command: SerialAPISetupCommand;
public serialize(): Buffer {
this.payload = Buffer.concat([
Buffer.from([this.command]),
this.payload,
]);
return super.serialize();
}
public toLogEntry(): MessageOrCCLogEntry {
const message: MessageRecord = {
command: getEnumMemberName(SerialAPISetupCommand, this.command),
};
if (this.payload.length > 0) {
message.payload = `0x${this.payload.toString("hex")}`;
}
return {
...super.toLogEntry(),
message,
};
}
}
export class SerialAPISetupResponse extends Message {
public constructor(
host: ZWaveHost,
options: MessageDeserializationOptions,
) {
super(host, options);
this.command = this.payload[0];
const CommandConstructor = getSubCommandResponseConstructor(
this.command,
);
if (CommandConstructor && (new.target as any) !== CommandConstructor) {
return new CommandConstructor(host, options);
}
this.payload = this.payload.slice(1);
}
public command: SerialAPISetupCommand;
public toLogEntry(): MessageOrCCLogEntry {
const message: MessageRecord = {
command: getEnumMemberName(SerialAPISetupCommand, this.command),
};
if (this.payload.length > 0) {
message.payload = `0x${this.payload.toString("hex")}`;
}
return {
...super.toLogEntry(),
message,
};
}
}
export class SerialAPISetup_CommandUnsupportedResponse extends SerialAPISetupResponse {
public constructor(
host: ZWaveHost,
options: MessageDeserializationOptions,
) {
super(host, options);
// The payload contains which command is unsupported
this.command = this.payload[0];
}
public toLogEntry(): MessageOrCCLogEntry {
const ret = { ...super.toLogEntry() };
const message = ret.message!;
message.error = "unsupported command";
message.command = getEnumMemberName(
SerialAPISetupCommand,
this.command,
);
delete message.payload;
return ret;
}
}
// =============================================================================
export class SerialAPISetup_GetSupportedCommandsRequest extends SerialAPISetupRequest {
public constructor(host: ZWaveHost, options?: MessageOptions) {
super(host, options);
this.command = SerialAPISetupCommand.GetSupportedCommands;
}
}
export class SerialAPISetup_GetSupportedCommandsResponse extends SerialAPISetupResponse {
public constructor(
host: ZWaveHost,
options: MessageDeserializationOptions,
) {
super(host, options);
validatePayload(this.payload.length >= 1);
if (this.payload.length > 1) {
// This module supports the extended bitmask to report the supported serial API setup commands
// Parse it as a bitmask
this.supportedCommands = parseBitMask(
this.payload.slice(1),
// According to the Host API specification, the first bit (bit 0) should be GetSupportedCommands
// However, at least in Z-Wave SDK 7.15, the entire bitmask is shifted by 1 bit and
// GetSupportedCommands is encoded in the second bit (bit 1)
// TODO: When this is fixed, make the start value dependent on the SDK version
SerialAPISetupCommand.Unsupported,
);
} else {
// This module only uses the single byte power-of-2 bitmask. Decode it manually
this.supportedCommands = [];
for (const cmd of [
SerialAPISetupCommand.GetSupportedCommands,
SerialAPISetupCommand.SetTxStatusReport,
SerialAPISetupCommand.SetPowerlevel,
SerialAPISetupCommand.GetPowerlevel,
SerialAPISetupCommand.GetMaximumPayloadSize,
SerialAPISetupCommand.GetRFRegion,
SerialAPISetupCommand.SetRFRegion,
SerialAPISetupCommand.SetNodeIDType,
] as const) {
if (!!(this.payload[0] & cmd)) this.supportedCommands.push(cmd);
}
}
// Apparently GetSupportedCommands is not always included in the bitmask, although we
// just received a response to the command
if (
!this.supportedCommands.includes(
SerialAPISetupCommand.GetSupportedCommands,
)
) {
this.supportedCommands.unshift(
SerialAPISetupCommand.GetSupportedCommands,
);
}
}
public readonly supportedCommands: SerialAPISetupCommand[];
}
// =============================================================================
export interface SerialAPISetup_SetTXStatusReportOptions
extends MessageBaseOptions {
enabled: boolean;
}
export class SerialAPISetup_SetTXStatusReportRequest extends SerialAPISetupRequest {
public constructor(
host: ZWaveHost,
options:
| MessageDeserializationOptions
| SerialAPISetup_SetTXStatusReportOptions,
) {
super(host, options);
this.command = SerialAPISetupCommand.SetTxStatusReport;
if (gotDeserializationOptions(options)) {
throw new ZWaveError(
`${this.constructor.name}: deserialization not implemented`,
ZWaveErrorCodes.Deserialization_NotImplemented,
);
} else {
this.enabled = options.enabled;
}
}
public enabled: boolean;
public serialize(): Buffer {
this.payload = Buffer.from([this.enabled ? 0xff : 0x00]);
return super.serialize();
}
public toLogEntry(): MessageOrCCLogEntry {
const ret = { ...super.toLogEntry() };
const message = ret.message!;
message.enabled = this.enabled;
delete message.payload;
return ret;
}
}
export class SerialAPISetup_SetTXStatusReportResponse
extends SerialAPISetupResponse
implements SuccessIndicator
{
public constructor(
host: ZWaveHost,
options: MessageDeserializationOptions,
) {
super(host, options);
this.success = this.payload[0] !== 0;
}
isOK(): boolean {
return this.success;
}
public readonly success: boolean;
public toLogEntry(): MessageOrCCLogEntry {
const ret = { ...super.toLogEntry() };
const message = ret.message!;
message.success = this.success;
delete message.payload;
return ret;
}
}
// =============================================================================
export interface SerialAPISetup_SetNodeIDTypeOptions
extends MessageBaseOptions {
nodeIdType: NodeIDType;
}
export class SerialAPISetup_SetNodeIDTypeRequest extends SerialAPISetupRequest {
public constructor(
host: ZWaveHost,
options:
| MessageDeserializationOptions
| SerialAPISetup_SetNodeIDTypeOptions,
) {
super(host, options);
this.command = SerialAPISetupCommand.SetNodeIDType;
if (gotDeserializationOptions(options)) {
throw new ZWaveError(
`${this.constructor.name}: deserialization not implemented`,
ZWaveErrorCodes.Deserialization_NotImplemented,
);
} else {
this.nodeIdType = options.nodeIdType;
}
}
public nodeIdType: NodeIDType;
public serialize(): Buffer {
this.payload = Buffer.from([this.nodeIdType]);
return super.serialize();
}
public toLogEntry(): MessageOrCCLogEntry {
const ret = { ...super.toLogEntry() };
const message = ret.message!;
message["node ID type"] =
this.nodeIdType === NodeIDType.Short ? "8 bit" : "16 bit";
delete message.payload;
return ret;
}
}
export class SerialAPISetup_SetNodeIDTypeResponse
extends SerialAPISetupResponse
implements SuccessIndicator
{
public constructor(
host: ZWaveHost,
options: MessageDeserializationOptions,
) {
super(host, options);
this.success = this.payload[0] !== 0;
}
isOK(): boolean {
return this.success;
}
public readonly success: boolean;
public toLogEntry(): MessageOrCCLogEntry {
const ret = { ...super.toLogEntry() };
const message = ret.message!;
message.success = this.success;
delete message.payload;
return ret;
}
}
// =============================================================================
export class SerialAPISetup_GetRFRegionRequest extends SerialAPISetupRequest {
public constructor(host: ZWaveHost, options?: MessageOptions) {
super(host, options);
this.command = SerialAPISetupCommand.GetRFRegion;
}
}
export class SerialAPISetup_GetRFRegionResponse extends SerialAPISetupResponse {
public constructor(
host: ZWaveHost,
options: MessageDeserializationOptions,
) {
super(host, options);
this.region = this.payload[0];
}
public readonly region: RFRegion;
public toLogEntry(): MessageOrCCLogEntry {
const ret = { ...super.toLogEntry() };
const message = ret.message!;
message.region = getEnumMemberName(RFRegion, this.region);
delete message.payload;
return ret;
}
}
// =============================================================================
export interface SerialAPISetup_SetRFRegionOptions extends MessageBaseOptions {
region: RFRegion;
}
export class SerialAPISetup_SetRFRegionRequest extends SerialAPISetupRequest {
public constructor(
host: ZWaveHost,
options:
| MessageDeserializationOptions
| SerialAPISetup_SetRFRegionOptions,
) {
super(host, options);
this.command = SerialAPISetupCommand.SetRFRegion;
if (gotDeserializationOptions(options)) {
throw new ZWaveError(
`${this.constructor.name}: deserialization not implemented`,
ZWaveErrorCodes.Deserialization_NotImplemented,
);
} else {
this.region = options.region;
}
}
public region: RFRegion;
public serialize(): Buffer {
this.payload = Buffer.from([this.region]);
return super.serialize();
}
public toLogEntry(): MessageOrCCLogEntry {
const ret = { ...super.toLogEntry() };
const message = ret.message!;
message.region = getEnumMemberName(RFRegion, this.region);
delete message.payload;
return ret;
}
}
export class SerialAPISetup_SetRFRegionResponse
extends SerialAPISetupResponse
implements SuccessIndicator
{
public constructor(
host: ZWaveHost,
options: MessageDeserializationOptions,
) {
super(host, options);
this.success = this.payload[0] !== 0;
}
isOK(): boolean {
return this.success;
}
public readonly success: boolean;
public toLogEntry(): MessageOrCCLogEntry {
const ret = { ...super.toLogEntry() };
const message = ret.message!;
message.success = this.success;
delete message.payload;
return ret;
}
}
// =============================================================================
export class SerialAPISetup_GetPowerlevelRequest extends SerialAPISetupRequest {
public constructor(host: ZWaveHost, options?: MessageOptions) {
super(host, options);
this.command = SerialAPISetupCommand.GetPowerlevel;
}
}
export class SerialAPISetup_GetPowerlevelResponse extends SerialAPISetupResponse {
public constructor(
host: ZWaveHost,
options: MessageDeserializationOptions,
) {
super(host, options);
validatePayload(this.payload.length >= 2);
// The values are in 0.1 dBm, signed
this.powerlevel = this.payload.readInt8(0) / 10;
this.measured0dBm = this.payload.readInt8(1) / 10;
}
/** The configured normal powerlevel in dBm */
public readonly powerlevel: number;
/** The measured output power in dBm for a normal output powerlevel of 0 */
public readonly measured0dBm: number;
public toLogEntry(): MessageOrCCLogEntry {
const ret = { ...super.toLogEntry() };
const message: MessageRecord = {
...ret.message!,
"normal powerlevel": `${this.powerlevel.toFixed(1)} dBm`,
"output power at 0 dBm": `${this.measured0dBm.toFixed(1)} dBm`,
};
delete message.payload;
ret.message = message;
return ret;
}
}
// =============================================================================
export interface SerialAPISetup_SetPowerlevelOptions
extends MessageBaseOptions {
powerlevel: number;
measured0dBm: number;
}
export class SerialAPISetup_SetPowerlevelRequest extends SerialAPISetupRequest {
public constructor(
host: ZWaveHost,
options:
| MessageDeserializationOptions
| SerialAPISetup_SetPowerlevelOptions,
) {
super(host, options);
this.command = SerialAPISetupCommand.SetPowerlevel;
if (gotDeserializationOptions(options)) {
throw new ZWaveError(
`${this.constructor.name}: deserialization not implemented`,
ZWaveErrorCodes.Deserialization_NotImplemented,
);
} else {
if (options.powerlevel < -12.8 || options.powerlevel > 12.7) {
throw new ZWaveError(
`The normal powerlevel must be between -12.8 and +12.7 dBm`,
ZWaveErrorCodes.Argument_Invalid,
);
}
if (options.measured0dBm < -12.8 || options.measured0dBm > 12.7) {
throw new ZWaveError(
`The measured output power at 0 dBm must be between -12.8 and +12.7 dBm`,
ZWaveErrorCodes.Argument_Invalid,
);
}
this.powerlevel = options.powerlevel;
this.measured0dBm = options.measured0dBm;
}
}
public powerlevel: number;
public measured0dBm: number;
public serialize(): Buffer {
this.payload = Buffer.allocUnsafe(2);
// The values are in 0.1 dBm
this.payload.writeInt8(Math.round(this.powerlevel * 10), 0);
this.payload.writeInt8(Math.round(this.measured0dBm * 10), 1);
return super.serialize();
}
public toLogEntry(): MessageOrCCLogEntry {
const ret = { ...super.toLogEntry() };
const message: MessageRecord = {
...ret.message!,
"normal powerlevel": `${this.powerlevel.toFixed(1)} dBm`,
"output power at 0 dBm": `${this.measured0dBm.toFixed(1)} dBm`,
};
delete message.payload;
ret.message = message;
return ret;
}
}
export class SerialAPISetup_SetPowerlevelResponse
extends SerialAPISetupResponse
implements SuccessIndicator
{
public constructor(
host: ZWaveHost,
options: MessageDeserializationOptions,
) {
super(host, options);
this.success = this.payload[0] !== 0;
}
isOK(): boolean {
return this.success;
}
public readonly success: boolean;
public toLogEntry(): MessageOrCCLogEntry {
const ret = { ...super.toLogEntry() };
const message = ret.message!;
message.success = this.success;
delete message.payload;
return ret;
}
}
// =============================================================================
export class SerialAPISetup_GetMaximumPayloadSizeRequest extends SerialAPISetupRequest {
public constructor(host: ZWaveHost, options?: MessageOptions) {
super(host, options);
this.command = SerialAPISetupCommand.GetMaximumPayloadSize;
}
}
export class SerialAPISetup_GetMaximumPayloadSizeResponse extends SerialAPISetupResponse {
public constructor(
host: ZWaveHost,
options: MessageDeserializationOptions,
) {
super(host, options);
this.maxPayloadSize = this.payload[0];
}
public readonly maxPayloadSize: number;
public toLogEntry(): MessageOrCCLogEntry {
const ret = { ...super.toLogEntry() };
const message = ret.message!;
message["maximum payload size"] = `${this.maxPayloadSize} bytes`;
delete message.payload;
return ret;
}
}
// =============================================================================
export class SerialAPISetup_GetLRMaximumPayloadSizeRequest extends SerialAPISetupRequest {
public constructor(host: ZWaveHost, options?: MessageOptions) {
super(host, options);
this.command = SerialAPISetupCommand.GetLRMaximumPayloadSize;
}
}
export class SerialAPISetup_GetLRMaximumPayloadSizeResponse extends SerialAPISetupResponse {
public constructor(
host: ZWaveHost,
options: MessageDeserializationOptions,
) {
super(host, options);
this.maxPayloadSize = this.payload[0];
}
public readonly maxPayloadSize: number;
public toLogEntry(): MessageOrCCLogEntry {
const ret = { ...super.toLogEntry() };
const message = ret.message!;
message["maximum payload size"] = `${this.maxPayloadSize} bytes`;
delete message.payload;
return ret;
}
}