diff --git a/packages/core/src/capabilities/RFRegion.ts b/packages/core/src/capabilities/RFRegion.ts index aba07d80d5f6..1d615d3554f8 100644 --- a/packages/core/src/capabilities/RFRegion.ts +++ b/packages/core/src/capabilities/RFRegion.ts @@ -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, diff --git a/packages/zwave-js/src/lib/controller/Controller.ts b/packages/zwave-js/src/lib/controller/Controller.ts index 89e29ee5bb33..50dbf21a7319 100644 --- a/packages/zwave-js/src/lib/controller/Controller.ts +++ b/packages/zwave-js/src/lib/controller/Controller.ts @@ -52,6 +52,7 @@ import { ProtocolType, Protocols, RFRegion, + type RFRegionInfo, type RSSI, type Route, RouteKind, @@ -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, @@ -699,6 +704,14 @@ export class ZWaveController return this._supportsTimers; } + private _supportedRegions: MaybeNotKnown>; + /** Which RF regions are supported by the controller, including information about them */ + public get supportedRegions(): MaybeNotKnown< + ReadonlyMap> + > { + return this._supportedRegions; + } + private _rfRegion: MaybeNotKnown; /** 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 { @@ -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()) { @@ -1298,6 +1322,58 @@ export class ZWaveController * Queries the region and powerlevel settings and configures them if necessary */ public async queryAndConfigureRF(): Promise { + // 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( @@ -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 { + 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. * @@ -6292,7 +6418,25 @@ ${associatedNodes.join(", ")}`, public getSupportedRFRegions( filterSubsets: boolean = true, ): MaybeNotKnown { - // 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, diff --git a/packages/zwave-js/src/lib/serialapi/capability/SerialAPISetupMessages.ts b/packages/zwave-js/src/lib/serialapi/capability/SerialAPISetupMessages.ts index 8311e1783b63..a783fb392a31 100644 --- a/packages/zwave-js/src/lib/serialapi/capability/SerialAPISetupMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/capability/SerialAPISetupMessages.ts @@ -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 @@ -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; + } +}