diff --git a/packages/cc/src/cc/index.ts b/packages/cc/src/cc/index.ts index 481160696285..a0e864776beb 100644 --- a/packages/cc/src/cc/index.ts +++ b/packages/cc/src/cc/index.ts @@ -1075,3 +1075,4 @@ export { FibaroVenetianBlindCCReport, FibaroVenetianBlindCCSet, } from "./manufacturerProprietary/FibaroCC.js"; +export { IntermaticPE653CC } from "./manufacturerProprietary/IntermaticPE653CC.js"; diff --git a/packages/cc/src/cc/manufacturerProprietary/IntermaticPE653CC.ts b/packages/cc/src/cc/manufacturerProprietary/IntermaticPE653CC.ts new file mode 100644 index 000000000000..d622a24845d9 --- /dev/null +++ b/packages/cc/src/cc/manufacturerProprietary/IntermaticPE653CC.ts @@ -0,0 +1,171 @@ +import { + CommandClasses, + type GetValueDB, + type MessageOrCCLogEntry, + type MessageRecord, + type ValueID, + ValueMetadata, + type WithAddress, + ZWaveError, + ZWaveErrorCodes, + validatePayload, +} from "@zwave-js/core/safe"; +import { type CCEncodingContext, type CCParsingContext } from "@zwave-js/cc"; +import { Bytes } from "@zwave-js/shared"; +import { validateArgs } from "@zwave-js/transformers"; +import { + POLL_VALUE, + type PollValueImplementation, + SET_VALUE, + type SetValueImplementation, + throwUnsupportedProperty, + throwWrongValueType, +} from "../../lib/API.js"; +import { + type CCRaw, + type CommandClassOptions, + type InterviewContext, + type PersistValuesContext, + type RefreshValuesContext, +} from "../../lib/CommandClass.js"; +import { + ManufacturerProprietaryCC, + ManufacturerProprietaryCCAPI, +} from "../ManufacturerProprietaryCC.js"; +import { + manufacturerId, + manufacturerProprietaryAPI, +} from "./Decorators.js"; + +export const MANUFACTURERID_INTERMATIC_LEGACY = 0x0005; +export const MANUFACTURERID_INTERMATIC_NEW = 0x0072; + +/** Returns the ValueID used to store the current water temperature */ +export function getIntermaticWaterTempValueId(): ValueID { + return { + commandClass: CommandClasses["Manufacturer Proprietary"], + property: "intermatic", + propertyKey: "waterTemperature", + }; +} + +/** Returns the value metadata for water temperature */ +export function getIntermaticWaterTempMetadata(): ValueMetadata { + return { + ...ValueMetadata.Number, + label: "Water Temperature", + unit: "°F", + min: 0, + max: 100, + }; +} + +@manufacturerProprietaryAPI(MANUFACTURERID_INTERMATIC_LEGACY) +export class IntermaticPE653CCAPI extends ManufacturerProprietaryCCAPI { + public async getWaterTemperature(): Promise { + const valueId = getIntermaticWaterTempValueId(); + return this.getValueDB()?.getValue(valueId); + } + + protected get [SET_VALUE](): SetValueImplementation { + return async function( + this: IntermaticPE653CCAPI, + { property }, + value, + ) { + if (property !== "intermatic") { + throwUnsupportedProperty(this.ccId, property); + } + + // This is a read-only CC + throwWrongValueType(this.ccId, property, "readonly", typeof value); + }; + } + + protected get [POLL_VALUE](): PollValueImplementation { + return async function(this: IntermaticPE653CCAPI, { property }) { + if (property !== "intermatic") { + throwUnsupportedProperty(this.ccId, property); + } + + return this.getWaterTemperature(); + }; + } +} + +@manufacturerId(MANUFACTURERID_INTERMATIC_LEGACY) +export class IntermaticPE653CC extends ManufacturerProprietaryCC { + public constructor(options: CommandClassOptions) { + super(options); + this.manufacturerId = MANUFACTURERID_INTERMATIC_LEGACY; + } + + public static from(raw: CCRaw, ctx: CCParsingContext): IntermaticPE653CC { + // Check if the manufacturer ID matches either the legacy or new ID + const ccCommand = raw.ccCommand ?? 0; + const manufacturerId = (ccCommand << 8) | raw.payload[0]; + if (manufacturerId !== MANUFACTURERID_INTERMATIC_LEGACY && + manufacturerId !== MANUFACTURERID_INTERMATIC_NEW) { + throw new ZWaveError( + `Invalid manufacturer ID for Intermatic PE653: ${manufacturerId}`, + ZWaveErrorCodes.Argument_Invalid + ); + } + + const cc = new IntermaticPE653CC({ + nodeId: ctx.sourceNodeId, + }); + cc.manufacturerId = manufacturerId; + + // Validate payload length for PE653 v3.1 water temperature frame + validatePayload(raw.payload.length >= 13, "PE653 frame too short"); + + // Parse the payload according to the Intermatic PE653 protocol + const waterTemp = raw.payload[12]; + cc.waterTemperature = waterTemp; + + return cc; + } + + public waterTemperature?: number; + + public async interview(ctx: InterviewContext): Promise { + // No interview needed for this CC + this.setInterviewComplete(ctx, true); + } + + public async refreshValues(ctx: RefreshValuesContext): Promise { + // This CC is read-only and updates are received automatically + // Skip refresh since this is a read-only CC + } + + public persistValues(ctx: PersistValuesContext): boolean { + if (this.waterTemperature != undefined) { + const valueId = getIntermaticWaterTempValueId(); + const metadata = getIntermaticWaterTempMetadata(); + + this.getValueDB(ctx)?.setMetadata(valueId, metadata); + this.getValueDB(ctx)?.setValue(valueId, this.waterTemperature); + return true; + } + return false; + } + + public serialize(ctx: CCEncodingContext): Bytes { + // The manufacturer ID is encoded in the first two bytes + const manufacturerId = this.manufacturerId ?? MANUFACTURERID_INTERMATIC_LEGACY; + (this.ccCommand as unknown as number) = (manufacturerId >>> 8) & 0xff; + this.payload = Bytes.from([manufacturerId & 0xff]); + return super.serialize(ctx); + } + + public toLogEntry(ctx?: GetValueDB): MessageOrCCLogEntry { + const message: MessageRecord = { + waterTemperature: this.waterTemperature ?? "unknown", + }; + return { + ...super.toLogEntry(ctx), + message, + }; + } +} \ No newline at end of file diff --git a/packages/cc/src/cc/manufacturerProprietary/index.ts b/packages/cc/src/cc/manufacturerProprietary/index.ts new file mode 100644 index 000000000000..ac6cbd2ef780 --- /dev/null +++ b/packages/cc/src/cc/manufacturerProprietary/index.ts @@ -0,0 +1,3 @@ +export * from "./Decorators.js"; +export * from "./FibaroCC.js"; +export * from "./IntermaticPE653CC.js"; \ No newline at end of file diff --git a/packages/config/config/devices/0x0005/pe653.json b/packages/config/config/devices/0x0072/pe653.json similarity index 85% rename from packages/config/config/devices/0x0005/pe653.json rename to packages/config/config/devices/0x0072/pe653.json index c3cba1d336ae..4696e521a65b 100644 --- a/packages/config/config/devices/0x0005/pe653.json +++ b/packages/config/config/devices/0x0072/pe653.json @@ -1,18 +1,39 @@ { - "manufacturer": "Intermatic", - "manufacturerId": "0x0005", - "label": "PE653", - "description": "Pool Control", + "manufacturer": "Intermatic 0x0072 WaterTemp", + "manufacturerId": "0x0072", + "label": "PE653 0x0072 WaterTemp", + "description": "Pool Control 0x0072 WaterTemp", "devices": [ { - "productType": "0x5045", - "productId": "0x0653" + "productType": "0x0500", + "productId": "0x7205" } ], "firmwareVersion": { "min": "0.0", "max": "255.255" }, + "proprietary": { + "manufacturerProprietaryConfig": { + "manufacturerId": 114, + "commandClasses": { + "waterTemperature": { + "type": "number", + "unit": "°F", + "min": 0, + "max": 100, + "readOnly": true, + "valueChangeOptions": ["always"], + "stateless": false, + "isFromConfig": true, + "ccSpecific": { + "propertyName": "waterTemperature", + "propertyKey": "waterTemperature" + } + } + } + } + }, "paramInformation": [ { "#": "1[0x02]", @@ -481,72 +502,64 @@ }, "compat": [ { - // Fixes #4588: Firmware v3.4 has numerous bugs related to multi-endpoint support. - // Firmware v3.3 and v3.1 do not appear to have the same issues. "$if": "firmwareVersion === 3.4", "commandClasses": { - // Force use of Multi Channel CC V1 despite the device reporting V2 "add": { "Multi Channel": { "isSupported": true, "version": 1 } }, - // The firmware handles requests on some endpoints incorrectly, often reporting garbage - // that confuses discovery or inhibits operation. Remove all of these broken CCs. "remove": { - // BasicCC: All endpoints control the state of Switch 1 so only keep the root endpoint - // to reduce clutter and to handle received BASIC_SET events. "Basic": { "endpoints": [1, 2, 3, 4, 5] }, - // ManufacturerSpecificCC: Endpoint 1 erroneously reports an incorrect manufacturer - // and product ID, unlike on the root endpoint. "Manufacturer Specific": { "endpoints": [1] }, - // ClockCC: Endpoint 1 erroneously reports a time with an invalid minute field, - // unlike on the root endpoint. "Clock": { "endpoints": [1] }, - // AssociationCC: Endpoint 1 erroneously reports that it supports 133 associated nodes - // but association commands don't work at all, unlike on the root endpoint. "Association": { "endpoints": [1] }, - // VersionCC: Endpoint 1 reports an unknown version, unlike on the root endpoint. "Version": { "endpoints": [1] } } }, - // The device sometimes sends BASIC_SET to the lifeline association when the state of Switch 1 - // changes but the value is always 0 so treat it as an event. "mapBasicSet": "event" }, { "commandClasses": { - // Force use of Multi Channel CC V1 despite the device reporting V2 "add": { "Multi Channel": { "isSupported": true, "version": 1 + }, + "Manufacturer Proprietary": { + "isSupported": true, + "version": 1, + "optional": false } } - }, - "overrideQueries": { - // The response to the setpoint query is off by one bit: https://github.com/zwave-js/node-zwave-js/issues/5335 - "Thermostat Setpoint": [ - { - "method": "getSupportedSetpointTypes", - "result": [ - 1, // Heating - 7 // Furnace - ] - } - ] } } - ] + ], + "endpoints": { + "0": { + "commandClasses": { + "0x72": { + "isSupported": true, + "version": 1 + }, + "0x91": { + "isSupported": true, + "version": 1, + "optional": false + } + } + } + } } + diff --git a/packages/intermatic-pe653/src/IntermaticPE653CC.ts b/packages/intermatic-pe653/src/IntermaticPE653CC.ts new file mode 100644 index 000000000000..6e4836233967 --- /dev/null +++ b/packages/intermatic-pe653/src/IntermaticPE653CC.ts @@ -0,0 +1,55 @@ +import { + CommandClass, + ManufacturerProprietaryCC, + CCAPI, + ValueMetadata, + ValueID, +} from "zwave-js"; + +const INTERMATIC_MANUFACTURER_ID = 0x0005; + +export class IntermaticPE653CC extends ManufacturerProprietaryCC { + public static readonly manufacturerId = INTERMATIC_MANUFACTURER_ID; + public static readonly version = 1; + + public deserialize(data: Buffer): void { + super.deserialize(data); + + if (data.length >= 13) { + const waterTemp = data.readUInt8(12); + this.persistWaterTemp(waterTemp); + } + } + + private persistWaterTemp(waterTemp: number): void { + const valueId: ValueID = { + commandClass: this.ccId, + property: "waterTemperature", + }; + + const metadata: ValueMetadata = { + label: "Water Temperature", + unit: "°F", + valueType: "number", + min: 0, + max: 100, + }; + + this.getValueDB()?.setMetadata(valueId, metadata); + this.getValueDB()?.setValue(valueId, waterTemp); + } +} + +export class IntermaticPE653CCAPI extends CCAPI { + public async getWaterTemperature(): Promise { + const valueId: ValueID = { + commandClass: this.ccId, + property: "waterTemperature", + }; + + return this.getValueDB()?.getValue(valueId); + } +} + +// Register this CC with Z-Wave JS +ManufacturerProprietaryCC.addImplementation(INTERMATIC_MANUFACTURER_ID, IntermaticPE653CC); \ No newline at end of file diff --git a/packages/transformers/src/validateArgs/transformDecorators.ts b/packages/transformers/src/validateArgs/transformDecorators.ts index f5b3b6780524..9d83b0220939 100644 --- a/packages/transformers/src/validateArgs/transformDecorators.ts +++ b/packages/transformers/src/validateArgs/transformDecorators.ts @@ -1,6 +1,7 @@ import path from "node:path"; -import { type PluginConfig } from "ts-patch"; -import ts, { type TransformerExtras } from "typescript"; +import type { PluginConfig } from "ts-patch"; +import type { TransformerExtras } from "ts-patch"; +import ts from "typescript"; /** * Transformer to replace @validateArgs() calls with the version @@ -11,7 +12,7 @@ import ts, { type TransformerExtras } from "typescript"; export default function transformer( program: ts.Program, pluginConfig: PluginConfig, - { ts: t }: TransformerExtras, + { ts: typescript }: TransformerExtras, ): ts.TransformerFactory { const compilerOptions = program.getCompilerOptions(); // Only enable the transforms if the custom condition is not set @@ -25,15 +26,11 @@ export default function transformer( // Bail early if there is no import for "@zwave-js/transformers". In this case, there's nothing to transform if (!file.getFullText().includes("@zwave-js/transformers")) { - // if (options?.verbose) { - // console.log( - // `@zwave-js/transformers not imported in ${file.fileName}, skipping`, - // ); - // } return file; } const f = context.factory; + const t = typescript; let className: string | undefined; let methodName: string | undefined; @@ -48,7 +45,7 @@ export default function transformer( } if (className && t.isMethodDeclaration(node) && node.name) { - methodName = node.name.getText(); + methodName = t.isIdentifier(node.name) ? node.name.text : node.name.getText(); const ret = t.visitEachChild(node, renameDecorators, context); methodName = undefined; return ret; @@ -81,7 +78,7 @@ export default function transformer( // Remove @zwave-js/transformers import const selfImports = file.statements - .filter((s) => ts.isImportDeclaration(s)) + .filter((s): s is ts.ImportDeclaration => t.isImportDeclaration(s)) .filter( (i) => i.moduleSpecifier @@ -132,14 +129,15 @@ export default function transformer( ); if (selfImports.length > 0) { + const nonImportStatements = file.statements.filter((s) => + !selfImports.includes(s as ts.ImportDeclaration) + ); file = context.factory.updateSourceFile( file, [ newImport, destructure, - ...file.statements.filter((s) => - !selfImports.includes(s as any) - ), + ...nonImportStatements, ], file.isDeclarationFile, file.referencedFiles,