Skip to content

Commit

Permalink
feat: query supported RF regions and their info (#7118)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlCalzone committed Aug 19, 2024
1 parent c6a374e commit bdd9910
Show file tree
Hide file tree
Showing 3 changed files with 281 additions and 3 deletions.
7 changes: 7 additions & 0 deletions packages/core/src/capabilities/RFRegion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ export enum RFRegion {
"Default (EU)" = 0xff,
}

export interface RFRegionInfo {
region: RFRegion;
supportsZWave: boolean;
supportsLongRange: boolean;
includesRegion?: RFRegion;
}

export enum ZnifferRegion {
"Europe" = 0x00,
"USA" = 0x01,
Expand Down
150 changes: 147 additions & 3 deletions packages/zwave-js/src/lib/controller/Controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
ProtocolType,
Protocols,
RFRegion,
type RFRegionInfo,
type RSSI,
type Route,
RouteKind,
Expand Down Expand Up @@ -187,8 +188,12 @@ import {
type SerialAPISetup_GetPowerlevelResponse,
SerialAPISetup_GetRFRegionRequest,
type SerialAPISetup_GetRFRegionResponse,
SerialAPISetup_GetRegionInfoRequest,
type SerialAPISetup_GetRegionInfoResponse,
SerialAPISetup_GetSupportedCommandsRequest,
type SerialAPISetup_GetSupportedCommandsResponse,
SerialAPISetup_GetSupportedRegionsRequest,
type SerialAPISetup_GetSupportedRegionsResponse,
SerialAPISetup_SetLongRangeMaximumTxPowerRequest,
type SerialAPISetup_SetLongRangeMaximumTxPowerResponse,
SerialAPISetup_SetNodeIDTypeRequest,
Expand Down Expand Up @@ -699,6 +704,14 @@ export class ZWaveController
return this._supportsTimers;
}

private _supportedRegions: MaybeNotKnown<Map<RFRegion, RFRegionInfo>>;
/** Which RF regions are supported by the controller, including information about them */
public get supportedRegions(): MaybeNotKnown<
ReadonlyMap<RFRegion, Readonly<RFRegionInfo>>
> {
return this._supportedRegions;
}

private _rfRegion: MaybeNotKnown<RFRegion>;
/** Which RF region the controller is currently set to, or `undefined` if it could not be determined (yet). This value is cached and can be changed through {@link setRFRegion}. */
public get rfRegion(): MaybeNotKnown<RFRegion> {
Expand Down Expand Up @@ -1282,8 +1295,19 @@ export class ZWaveController

/** Tries to determine the LR capable replacement of the given region. If none is found, the given region is returned. */
private tryGetLRCapableRegion(region: RFRegion): RFRegion {
// There is no official API to query whether a given region is supported,
// but there are ways to figure out if LR regions are.
if (this._supportedRegions) {
// If the region supports LR, use it
if (this._supportedRegions.get(region)?.supportsLongRange) {
return region;
}

// Find a possible LR capable superset for this region
for (const info of this._supportedRegions.values()) {
if (info.supportsLongRange && info.includesRegion === region) {
return info.region;
}
}
}

// US_LR is the first supported LR region, so if the controller supports LR, US_LR is supported
if (region === RFRegion.USA && this.isLongRangeCapable()) {
Expand All @@ -1298,6 +1322,58 @@ export class ZWaveController
* Queries the region and powerlevel settings and configures them if necessary
*/
public async queryAndConfigureRF(): Promise<void> {
// Figure out which regions are supported
if (
this.isSerialAPISetupCommandSupported(
SerialAPISetupCommand.GetSupportedRegions,
)
) {
this.driver.controllerLog.print(
`Querying supported RF regions and their information...`,
);
const supportedRegions = await this.querySupportedRFRegions().catch(
() => [],
);
this._supportedRegions = new Map();

for (const region of supportedRegions) {
try {
const info = await this.queryRFRegionInfo(region);
if (info.region === RFRegion.Unknown) continue;
this._supportedRegions.set(region, info);
} catch {
continue;
}
}

this.driver.controllerLog.print(
`supported regions:${
[...this._supportedRegions.values()]
.map((info) => {
let ret = `\n· ${
getEnumMemberName(RFRegion, info.region)
}`;
if (info.includesRegion != undefined) {
ret += ` · superset of ${
getEnumMemberName(
RFRegion,
info.includesRegion,
)
}`;
}
if (info.supportsLongRange) {
ret += " · ZWLR";
if (!info.supportsZWave) {
ret += " only";
}
}
return ret;
})
.join("")
}`,
);
}

// Check and possibly update the RF region to the desired value
if (
this.isSerialAPISetupCommandSupported(
Expand Down Expand Up @@ -6283,6 +6359,56 @@ ${associatedNodes.join(", ")}`,
return result.region;
}

/**
* Query the supported regions of the Z-Wave API Module
*
* **Note:** Applications should prefer using {@link getSupportedRFRegions} instead
*/
public async querySupportedRFRegions(): Promise<RFRegion[]> {
const result = await this.driver.sendMessage<
| SerialAPISetup_GetSupportedRegionsResponse
| SerialAPISetup_CommandUnsupportedResponse
>(new SerialAPISetup_GetSupportedRegionsRequest(this.driver));
if (result instanceof SerialAPISetup_CommandUnsupportedResponse) {
throw new ZWaveError(
`Your hardware does not support getting the supported RF regions!`,
ZWaveErrorCodes.Driver_NotSupported,
);
}
return result.supportedRegions;
}

/**
* Query the supported regions of the Z-Wave API Module
*
* **Note:** Applications should prefer reading the cached value from {@link supportedRFRegions} instead
*/
public async queryRFRegionInfo(
region: RFRegion,
): Promise<{
region: RFRegion;
supportsZWave: boolean;
supportsLongRange: boolean;
includesRegion?: RFRegion;
}> {
const result = await this.driver.sendMessage<
| SerialAPISetup_GetRegionInfoResponse
| SerialAPISetup_CommandUnsupportedResponse
>(new SerialAPISetup_GetRegionInfoRequest(this.driver, { region }));
if (result instanceof SerialAPISetup_CommandUnsupportedResponse) {
throw new ZWaveError(
`Your hardware does not support getting the RF region info!`,
ZWaveErrorCodes.Driver_NotSupported,
);
}
return pick(result, [
"region",
"supportsZWave",
"supportsLongRange",
"includesRegion",
]);
}

/**
* Returns the RF regions supported by this controller, or `undefined` if the information is not known yet.
*
Expand All @@ -6292,7 +6418,25 @@ ${associatedNodes.join(", ")}`,
public getSupportedRFRegions(
filterSubsets: boolean = true,
): MaybeNotKnown<readonly RFRegion[]> {
// FIXME: Once supported in firmware, query the controller for supported regions instead of hardcoding
// If supported by the firmware, rely on the queried information
if (
this.isSerialAPISetupCommandSupported(
SerialAPISetupCommand.GetSupportedRegions,
)
) {
if (this._supportedRegions == NOT_KNOWN) return NOT_KNOWN;
const allRegions = new Set(this._supportedRegions.keys());
if (filterSubsets) {
for (const region of this._supportedRegions.values()) {
if (region.includesRegion != undefined) {
allRegions.delete(region.includesRegion);
}
}
}
return [...allRegions].sort((a, b) => a - b);
}

// Fallback: Hardcoded list of known supported regions
const ret = new Set([
// Always supported
RFRegion.Europe,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export enum SerialAPISetupCommand {
GetLongRangeMaximumPayloadSize = 0x11,
SetPowerlevel16Bit = 0x12,
GetPowerlevel16Bit = 0x13,
GetSupportedRegions = 0x15,
GetRegionInfo = 0x16,
}

// We need to define the decorators for Requests and Responses separately
Expand Down Expand Up @@ -983,3 +985,128 @@ export class SerialAPISetup_GetLongRangeMaximumPayloadSizeResponse
return ret;
}
}

// =============================================================================

@subCommandRequest(SerialAPISetupCommand.GetSupportedRegions)
export class SerialAPISetup_GetSupportedRegionsRequest
extends SerialAPISetupRequest
{
public constructor(host: ZWaveHost, options?: MessageOptions) {
super(host, options);
this.command = SerialAPISetupCommand.GetSupportedRegions;
}
}

@subCommandResponse(SerialAPISetupCommand.GetSupportedRegions)
export class SerialAPISetup_GetSupportedRegionsResponse
extends SerialAPISetupResponse
{
public constructor(
host: ZWaveHost,
options: MessageDeserializationOptions,
) {
super(host, options);
validatePayload(this.payload.length >= 1);

const numRegions = this.payload[0];
validatePayload(numRegions > 0, this.payload.length >= 1 + numRegions);

this.supportedRegions = [...this.payload.subarray(1, 1 + numRegions)];
}

public readonly supportedRegions: RFRegion[];
}

// =============================================================================

export interface SerialAPISetup_GetRegionInfoOptions
extends MessageBaseOptions
{
region: RFRegion;
}

@subCommandRequest(SerialAPISetupCommand.GetRegionInfo)
export class SerialAPISetup_GetRegionInfoRequest extends SerialAPISetupRequest {
public constructor(
host: ZWaveHost,
options:
| MessageDeserializationOptions
| SerialAPISetup_GetRegionInfoOptions,
) {
super(host, options);
this.command = SerialAPISetupCommand.GetRegionInfo;

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: MessageRecord = {
...ret.message!,
region: getEnumMemberName(RFRegion, this.region),
};
delete message.payload;
ret.message = message;
return ret;
}
}

@subCommandResponse(SerialAPISetupCommand.GetRegionInfo)
export class SerialAPISetup_GetRegionInfoResponse
extends SerialAPISetupResponse
{
public constructor(
host: ZWaveHost,
options: MessageDeserializationOptions,
) {
super(host, options);
this.region = this.payload[0];
this.supportsZWave = !!(this.payload[1] & 0b1);
this.supportsLongRange = !!(this.payload[1] & 0b10);
if (this.payload.length > 2) {
this.includesRegion = this.payload[2];
if (this.includesRegion === RFRegion.Unknown) {
this.includesRegion = undefined;
}
}
}

public readonly region: RFRegion;
public readonly supportsZWave: boolean;
public readonly supportsLongRange: boolean;
public readonly includesRegion?: RFRegion;

public toLogEntry(): MessageOrCCLogEntry {
const ret = { ...super.toLogEntry() };
const message: MessageRecord = {
...ret.message!,
region: getEnumMemberName(RFRegion, this.region),
"supports Z-Wave": this.supportsZWave,
"supports Long Range": this.supportsLongRange,
};
if (this.includesRegion != undefined) {
message["includes region"] = getEnumMemberName(
RFRegion,
this.includesRegion,
);
}
delete message.payload;
ret.message = message;
return ret;
}
}

0 comments on commit bdd9910

Please sign in to comment.