Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: query supported RF regions and their info #7118

Merged
merged 2 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
}
Loading