process . on ( "unhandledRejection" , ( r ) => { throw r ; } ) ; import { CommandClasses , getIntegerLimits } from "@zwave-js/core" ; import { enumFilesRecursive , formatId , getErrorMessage , num2hex , stringify , } from "@zwave-js/shared" ; import { composeObject } from "alcalzone-shared/objects" ; import { isArray , isObject } from "alcalzone-shared/typeguards" ; import { AssertionError , ok } from "assert" ; import axios from "axios" ; import * as child from "child_process" ; import * as JSONC from "comment-json" ; import * as fs from "fs-extra" ; import * as JSON5 from "json5" ; import * as path from "path" ; import { compare } from "semver" ; import { promisify } from "util" ; import xml2json from "xml2json" ; import yargs from "yargs" ; import { ConfigManager , DeviceConfigIndexEntry } from "../src" ; const execPromise = promisify ( child . exec ) ; const program = yargs . option ( "source" , { description : "source of the import" , alias : "s" , type : "array" , choices : [ "oh" , "ozw" , "zwa" ] , default : [ "zwa" ] } ) . option ( "ids" , { description : "devices ids to download. In ozw the format is '<manufacturer>-<productId>-<productType>'. Ex: '0x0086-0x0075-0x0004'" , type : "array" , array : true } ) . option ( "download" , { alias : "D" , description : "Download devices DB from <source>" , type : "boolean" , default : false } ) . option ( "clean" , { alias : "C" , description : "Clean temporary directory" , type : "boolean" , default : false } ) . option ( "manufacturers" , { alias : "m" , description : "Parse and update manufacturers.json" , type : "boolean" , default : false } ) . option ( "manufacturer_folder" , { alias : "M" , description : "Download all Z-Wave alliance files for the specified manufacturer (using the zwa website manufacturer ID)" , type : "array" , array : true } ) . option ( "devices" , { alias : "d" , description : "Parse and update devices configurations" , type : "boolean" , default : false } ) . option ( "parse" , { alias : "p" , description : "Run custom parse routines -- maintenance" , type : "boolean" , default : false } ) . example ( "import -s ozw -Dmd" , "Download and parse OpenZwave db (manufacturers, devices) and update the index" ) . example ( "import -s oh -Dmd" , "Download and parse openhab db (manufacturers, devices) and update the index" ) . example ( "import -s oh -D --ids 1234 5678" , "Download openhab devices with ids `1234` and `5678`" ) . help ( ) . version ( false ) . alias ( "h" , "help" ) . parseSync ( ) ; const processedDir = path . join ( __dirname , "../../../packages/config" , "config/devices" ) ; const configManager = new ConfigManager ( ) ; const ozwTempDir = path . join ( __dirname , "../../../.tmpozw" ) ; const ozwTarName = "openzwave.tar.gz" ; const ozwTarUrl = "https://github.com/OpenZWave/open-zwave/archive/master.tar.gz" ; const ozwConfigFolder = path . join ( ozwTempDir , "./config" ) ; const zwaTempDir = path . join ( __dirname , "../../../.tmpzwa" ) ; const ohTempDir = path . join ( __dirname , "../../../.tmpoh" ) ; const importedManufacturersPath = path . join ( ohTempDir , "manufacturers.json" ) ; const ohUrlManufacturers = "https://opensmarthouse.org/dmxConnect/api/zwavedatabase/manufacturers/list.php?sort=label&limit=99999" ; const ohUrlIDs = "https://opensmarthouse.org/dmxConnect/api/zwavedatabase/device/list.php?filter=&manufacturer=-1&limit=100000" ; const ohUrlDevice = ( id ) => `https://opensmarthouse.org/dmxConnect/api/zwavedatabase/device/read.php?device_id=${ id }` ; const zwaUrlDevice = ( id ) => `https://products.z-wavealliance.org/Products/${ id }/json` ; function isNullishOrEmptyString ( value ) { return value == undefined || value === "" ; } /** Updates a numeric value with a new value, sanitizing the input. Falls back to the previous value (if it exists) or a default one */ function updateNumberOrDefault ( newN , oldN , defaultN ) { let ret = sanitizeNumber ( newN ) ; if ( typeof ret === "number" ) return ret ; ret = sanitizeNumber ( oldN ) ; if ( typeof ret === "number" ) return ret ; return defaultN ; } /** Retrieves the list of database IDs from the OpenSmartHouse DB */ async function fetchIDsOH ( ) { const data = ( await axios ( { url : ohUrlIDs } ) ) . data ; return data . devices . map ( ( d ) => d . id ) ; } /** Retrieves the definition for a specific device from the OpenSmartHouse DB */ async function fetchDeviceOH ( id ) { const source = ( await axios ( { url : ohUrlDevice ( id ) } ) ) . data ; return stringify ( source , "\t" ) ; } /** Retrieves the definition for a specific device from the Z-Wave Alliance DB */ async function fetchDeviceZWA ( id ) { const source = ( await axios ( { url : zwaUrlDevice ( id ) } ) ) . data ; return stringify ( source , "\t" ) ; } /** Downloads ozw master archive and store it on `tmpDir` */ async function downloadOZWConfig ( ) { console . log ( "downloading ozw archive..." ) ; await fs . ensureDir ( ozwTempDir ) ; const data = ( await axios ( { url : ozwTarUrl , responseType : "stream" } ) ) . data ; return new Promise ( ( resolve , reject ) => { const fileDest = path . join ( ozwTempDir , ozwTarName ) ; const stream = fs . createWriteStream ( fileDest ) ; data . pipe ( stream ) ; let hasError = false ; stream . on ( "error" , ( err ) => { hasError = true ; stream . close ( ) ; reject ( err ) ; } ) ; stream . on ( "close" , ( ) => { if ( ! hasError ) { resolve ( fileDest ) ; console . log ( "ozw archive stored in temporary directory" ) ; } } ) ; } ) ; } /** Extract `config` folder from ozw archive in `tmpDir` */ async function extractConfigFromTar ( ) { console . log ( "extracting config folder from ozw archive..." ) ; await execPromise ( `tar -xzf ${ ozwTarName } open-zwave-master/config  --strip-components=1` , { cwd : ozwTempDir } ) ; } /** Delete all files in `tmpDir` */ async function cleanTmpDirectory ( ) { await fs . remove ( ozwTempDir ) ; await fs . remove ( ohTempDir ) ; await fs . remove ( zwaTempDir ) ; console . log ( "temporary directories cleaned" ) ; } function matchId ( manufacturer , prodType , prodId ) { return ! ! program . ids ?. includes ( `${ formatId ( manufacturer ) }-${ formatId ( prodType ) }-${ formatId ( prodId ) }` ) ; } /** Reads OZW `manufacturer_specific.xml` */ async function parseOZWConfig ( ) { const manufacturerFile = path . join ( ozwConfigFolder , "manufacturer_specific.xml" ) ; const manufacturerJson = xml2json . toJson ( await fs . readFile ( manufacturerFile , "utf8" ) , { object : true } ) ; await configManager . loadManufacturers ( ) ; if ( program . devices ) { await configManager . loadDeviceIndex ( ) ; } for ( const man of manufacturerJson . ManufacturerSpecificData . Manufacturer ) { const manufacturerId = parseInt ( man . id , 16 ) ; let manufacturerName = configManager . lookupManufacturer ( manufacturerId ) ; if ( manufacturerName === undefined && man . name !== undefined ) { console . log ( `Adding missing manufacturer: ${ man . name }` ) ; configManager . setManufacturer ( manufacturerId , man . name ) ; } manufacturerName = man . name ; if ( program . devices ) { const products = ensureArray ( man . Product ) ; for ( const product of products ) { if ( product . config !== undefined ) { if ( ! program . ids || matchId ( man . id , product . id , product . type ) ) { await parseOZWProduct ( product , manufacturerId , manufacturerName ) ; } } } } } if ( program . manufacturers ) { await configManager . saveManufacturers ( ) ; } } /**
 * When using xml2json some fields expected as array are parsed as objects
 * when there is only one element. This method ensures that they are arrays
 *
 */ function ensureArray ( json ) { json = json ?? [ ] ; return isArray ( json ) ? json : [ json ] ; } function normalizeUnits ( unit ) { if ( ! unit ) return undefined ; if ( /minutes/i . test ( unit ) ) { return "minutes" ; } else if ( /seconds/i . test ( unit ) ) { return "seconds" ; } else if ( /fahrenheit|\bf\b/i . test ( unit ) ) { return "°F" ; } else if ( /degrees celsius|celsius|\bc\b/i . test ( unit ) ) { return "°C" ; } else if ( /\bwatt/i . test ( unit ) ) { return "W" ; } else if ( /\bvolt/i . test ( unit ) ) { return "V" ; } else if ( /percent|dimmer level|%/i . test ( unit ) ) { return "%" ; } else if ( /degrees/i . test ( unit ) ) { return "°" ; } return unit ; } param config function normalizeConfig ( config ) { const topOrder = [ "manufacturer" , "manufacturerId" , "label" , "description" , "devices" , "firmwareVersion" , "associations" , "paramInformation" , "compat" , "metadata" , ] ; const paramOrder = [ "$if" , "$import" , "label" , "description" , "valueSize" , "unit" , "minValue" , "maxValue" , "defaultValue" , "unsigned" , "readOnly" , "writeOnly" , "allowManualEntry" , "options" , ] ; const disallowEmpty = [ "associations" , "paramInformation" , "compat" , "metadata" , ] ; /*******************
     * Standardize things
     ********************/ for ( const l of topOrder ) { if ( typeof config [ l ] === "undefined" ) { continue ; } else if ( config [ l ] === "" ) { delete config [ l ] ; } const temp = config [ l ] ; delete config [ l ] ; config [ l ] = temp ; } for ( const prop of Object . keys ( disallowEmpty ) ) { if ( prop in config ) { if ( isObject ( config [ prop ] ) && Object . keys ( config [ prop ] ) . length === 0 ) { delete config [ prop ] ; } else if ( isArray ( config [ prop ] ) && config [ prop ] . length === 0 ) { delete config [ prop ] ; } else if ( ! config [ prop ] ) { delete config [ prop ] ; } } } config . label = sanitizeText ( config . label ) ?? "" ; config . description = sanitizeText ( config . description ) ?? "" ; config . devices . sort ( ( a , b ) => { if ( a . productType < b . productType ) return - 1 ; if ( a . productType > b . productType ) return + 1 ; if ( a . productId < b . productId ) return - 1 ; if ( a . productId > b . productId ) return + 1 ; return 0 ; } ) ; if ( config . paramInformation ?. length ) { const allowedKeys = ( config . paramInformation as any [ ] ) . filter ( ( param , _ , arr ) => ! /^\d+$/ . test ( param [ "#" ] ) || "$if" in param || ! arr . some ( ( other ) => other [ "#" ] . startsWith ( `${ param [ "#" ] }[` ) ) ) . map ( ( e ) => e [ "#" ] ) ; config . paramInformation = config . paramInformation . filter ( ( param ) => allowedKeys . includes ( param [ "#" ] ) ) ; for ( const original of config . paramInformation ) { original . unit = normalizeUnits ( original . unit ) ; if ( original . readOnly ) { original . allowManualEntry = undefined ; original . writeOnly = undefined ; } else if ( original . writeOnly ) { original . readOnly = undefined ; } else { original . readOnly = undefined ; original . writeOnly = undefined ; } if ( original . allowManualEntry === true ) { original . allowManualEntry = undefined ; } for ( const l of paramOrder ) { if ( original [ l ] == undefined || original [ l ] === "" ) { delete original [ l ] ; continue ; } const temp = original [ l ] ; delete original [ l ] ; original [ l ] = temp ; } if ( original . options ?. length === 0 ) { delete original . options ; } else if ( program . source . includes ( "ozw" ) ) { const values = original . options . map ( ( o ) => o . value ) ; original . minValue = Math . min ( ... values ) ; original . maxValue = Math . max ( ... values ) ; } } } else { delete config . paramInformation ; } return config ; } param product async function parseOZWProduct ( product , manufacturerId , manufacturer ) { const productFile = await fs . readFile ( path . join ( ozwConfigFolder , product . config ) , "utf8" ) ; const productLabel = path . basename ( product . config , ".xml" ) . toLocaleUpperCase ( ) ; const productName = product . name . replace ( productLabel , "" ) ; product . id = product . id . replace ( /^0x/ , "" ) ; product . type = product . type . replace ( /^0x/ , "" ) ; const productId = formatId ( product . id ) ; const productType = formatId ( product . type ) ; const manufacturerIdHex = formatId ( manufacturerId ) ; const deviceConfigs = configManager . getIndex ( ) ?. filter ( ( f ) => f . manufacturerId === manufacturerIdHex && f . productType === productType && f . productId === productId ) ?? [ ] ; const latestConfig = getLatestConfigVersion ( deviceConfigs ) ; const fileNameRelative = latestConfig ?. filename ?? `${ manufacturerIdHex }/${ labelToFilename ( productLabel ) }.json` ; const fileNameAbsolute = path . join ( processedDir , fileNameRelative ) ; let existingDevice ; if ( await fs . pathExists ( fileNameAbsolute ) ) { existingDevice = JSON5 . parse ( await fs . readFile ( fileNameAbsolute , "utf8" ) ) ; } const json = xml2json . toJson ( productFile , { object : true , coerce : true } ) . Product as Record < string , any > ; const devices = existingDevice ?. devices ?? [ ] ; if ( ! devices . some ( ( d ) => d . productType === productType && d . productId === productId ) ) { devices . push ( { productType , productId } ) ; } const newConfig = { manufacturer , manufacturerId : manufacturerIdHex , label : productLabel , description : existingDevice ?. description ?? productName , devices : devices , firmwareVersion : { min : existingDevice ?. firmwareVersion . min ?? "0.0" , max : existingDevice ?. firmwareVersion . max ?? "255.255" } , associations : existingDevice ?. associations ?? { } , paramInformation : existingDevice ?. paramInformation ?? [ ] , compat : existingDevice ?. compat } ; if ( existingDevice ) { for ( const device of existingDevice . devices ) { if ( ! newConfig . devices . some ( ( d ) => d . productType === device . productType && d . productId === device . productId ) ) { newConfig . devices . push ( device ) ; } } } const commandClasses = ensureArray ( json . CommandClass ) ; const parameters = ensureArray ( commandClasses . find ( ( c ) => c . id === CommandClasses . Configuration ) ?. Value ) ; for ( const param of parameters ) { if ( isNaN ( param . index ) ) continue ; const isBitSet = param . type === "bitset" ; if ( isBitSet ) { const bitSetIds = ensureArray ( param . BitSet ) ; const defaultValue = typeof param . value === "number" ? param . value : 0 ; const valueSize = param . size || 1 ; param . label = ensureArray ( param . label ) [ 0 ] ; const paramLabel = param . label ? `${ param . label }. ` : "" ; for ( const bitSet of bitSetIds ) { const bit = ( bitSet . id || 1 ) - 1 ; const mask = 2 ** bit ; const id = `${ param . index }[${ num2hex ( mask ) }]` ; const label = ensureArray ( bitSet . Label ) [ 0 ] ?? "" ; const desc = ensureArray ( bitSet . Help ) [ 0 ] ?? "" ; const found = newConfig . paramInformation . find ( ( p ) => p [ "#" ] === id . toString ( ) ) ; const parsedParam = found ?? { } ; parsedParam . label = `${ paramLabel }${ label }` ; parsedParam . description = desc ; parsedParam . valueSize = valueSize ; parsedParam . minValue = 0 ; parsedParam . maxValue = 1 ; parsedParam . defaultValue = ! ! ( defaultValue & mask ) ? 1 : 0 ; parsedParam . readOnly = undefined ; parsedParam . writeOnly = undefined ; parsedParam . allowManualEntry = undefined ; if ( ! found ) newConfig . paramInformation . push ( parsedParam ) ; } } else { const found = newConfig . paramInformation . find ( ( p ) => p [ "#" ] === param . index . toString ( ) ) ; const parsedParam = found ?? { } ; parsedParam . label = ensureArray ( param . label ) [ 0 ] || parsedParam . label ; parsedParam . description = ensureArray ( param . Help ) [ 0 ] || parsedParam . description ; parsedParam . valueSize = updateNumberOrDefault ( param . size , parsedParam . valueSize , 1 ) ; parsedParam . minValue = updateNumberOrDefault ( param . min , parsedParam . min , 0 ) ; try { parsedParam . maxValue = updateNumberOrDefault ( param . max , parsedParam . max , getIntegerLimits ( parsedParam . valueSize , false ) . max ) ; } catch { parsedParam . maxValue = parsedParam . minValue ; } if ( param . read_only === true || param . read_only === "true" ) { parsedParam . readOnly = true ; } else if ( param . write_only === true || param . write_only === "true" ) { parsedParam . writeOnly = true ; } parsedParam . allowManualEntry = ! parsedParam . readOnly && param . type !== "list" ; parsedParam . defaultValue = updateNumberOrDefault ( param . value , parsedParam . value , parsedParam . minValue ) ; parsedParam . unsigned = true ; if ( param . units ) { parsedParam . unit = param . units ; } if ( isArray ( parsedParam . description ) ) { parsedParam . description = parsedParam . description [ 0 ] ; } if ( typeof parsedParam . description !== "string" ) { parsedParam . description = "" ; } const items = ensureArray ( param . Item ) ; if ( param . type === "list" && items . length > 0 ) { parsedParam . options = [ ] ; for ( const item of items ) { if ( ! parsedParam . options . find ( ( v ) => v . value === item . value ) ) { const opt = { label : item . label . toString ( ) , value : parseInt ( item . value ) } ; parsedParam . options . push ( opt ) ; } } } if ( ! found ) newConfig . paramInformation . push ( parsedParam ) ; } } const associations = [ ... ensureArray ( commandClasses . find ( ( c ) => c . id === CommandClasses . Association ) ?. Associations ?. Group ) , ... ensureArray ( commandClasses . find ( ( c ) => c . id === CommandClasses [ "Multi Channel Association" ] ) ?. Associations ?. Group ) , ] ; if ( associations . length > 0 ) { newConfig . associations ??= { } ; for ( const ass of associations ) { const parsedAssociation = newConfig . associations [ ass . index ] ?? { } ; parsedAssociation . label = ass . label ; parsedAssociation . maxNodes = ass . max_associations ; const isLifeline = /lifeline/i . test ( ass . label ) || ass . auto === "true" || ass . auto === true ; if ( isLifeline ) parsedAssociation . isLifeline = true ; newConfig . associations [ ass . index ] = parsedAssociation ; } } const toAdd = commandClasses . filter ( ( c ) => c . action === "add" ) . map ( ( c ) => c . id ) ; const toRemove = commandClasses . filter ( ( c ) => c . action === "remove" ) . map ( ( c ) => c . id ) ; if ( toAdd . length > 0 || toRemove . length > 0 ) { newConfig . compat ??= { } ; newConfig . compat . cc ??= { } ; if ( toAdd . length > 0 ) { newConfig . compat . cc . add = toAdd ; } if ( toRemove . length > 0 ) { newConfig . compat . cc . remove = toRemove ; } } const manufacturerDir = path . join ( processedDir , manufacturerIdHex ) ; await fs . ensureDir ( manufacturerDir ) ; const output = stringify ( normalizeConfig ( newConfig ) , "\t" ) + "\n" ; await fs . writeFile ( fileNameAbsolute , output , "utf8" ) ; } /*********************************************************
 *														 *
 *          zWave Alliance Processing Section            *
 * 														 *
 * *******************************************************/ /**
 * Parse a directory of zWave Alliance device xmls
 * */ async function parseZWAFiles ( ) { let jsonData = [ ] ; const configFiles = await enumFilesRecursive ( zwaTempDir , ( file ) => file . endsWith ( ".json" ) ) ; for ( const file of configFiles ) { const j = await fs . readFile ( file , "utf8" ) ; /**
         * zWave Alliance numbering isn't always continuous and an html page is
        returned when a device number doesn't. Test for and delete such files.
         */ if ( j . charAt ( 0 ) === "{" ) { jsonData . push ( JSON . parse ( j ) ) ; } else { void fs . unlink ( file ) ; } } jsonData = combineDeviceFiles ( jsonData ) ; jsonData = sanitizeFields ( jsonData ) ; await configManager . loadManufacturers ( ) ; if ( program . devices ) { await configManager . loadDeviceIndex ( ) ; } for ( const file of jsonData ) { const manufacturerId = parseInt ( file . ManufacturerId , 16 ) ; const manufacturerName = configManager . lookupManufacturer ( manufacturerId ) ; if ( Number . isNaN ( manufacturerId ) ) { } else if ( manufacturerName === undefined && file . Brand !== undefined ) { console . log ( `Adding missing manufacturer: ${ file . Brand }` ) ; configManager . setManufacturer ( manufacturerId , file . Brand ) ; } /**
         *  Process and write the device files, if called with program.devices
         */ if ( program . devices && file . ProductId ) { await parseZWAProduct ( file , manufacturerId , manufacturerName ) ; } } /**
     *  Write the manufacturer.json file, if called with program.manufacturers
     */ if ( program . manufacturers ) { await configManager . saveManufacturers ( ) ; } } /***
 * Combine zWave Alliance Device Files
 */ function combineDeviceFiles ( json ) { for ( const file of json ) { const identifier = file . Identifier ? file . Identifier : "Unknown" ; const normalizedIdentifier = normalizeIdentifier ( identifier ) ; file . Identifier = normalizedIdentifier [ 0 ] ; file . OriginalIdentifier = normalizedIdentifier [ 1 ] ; } for ( const file of json ) { const testManufactuer = file . ManufacturerId ; const testCertification = file . CertificationNumber ; const testParameters = file . ConfigurationParameters ; const testIdentifier = file . Identifier ; if ( ! file . ProductId ) { continue ; } file . ProductId = file . ProductId . replace ( /^0x/ , "" ) ; file . ProductId = formatId ( file . ProductId ) ; file . ProductTypeId = file . ProductTypeId . replace ( /^0x/ , "" ) ; file . ProductTypeId = formatId ( file . ProductTypeId ) ; for ( const test_file of json ) { if ( ! test_file . ProductId ) { continue ; } test_file . ProductId = test_file . ProductId . replace ( /^0x/ , "" ) ; test_file . ProductId = formatId ( test_file . ProductId ) ; test_file . ProductTypeId = test_file . ProductTypeId . replace ( /^0x/ , "" ) ; test_file . ProductTypeId = formatId ( test_file . ProductTypeId ) ; if ( test_file . ManufacturerId === testManufactuer && test_file . ProductId ) { if ( test_file . Identifier === testIdentifier && test_file . CertificationNumber === testCertification ) { file . combinedDevices = createOrUpdateArray ( file . combinedDevices , { ProductId : test_file . ProductId , ProductTypeId : test_file . ProductTypeId , Id : test_file . Id , Brand : test_file . Brand , Identifier : test_file . Identifier } ) ; } else if ( test_file . ProductId === file . ProductId && test_file . ProductTypeId === file . ProductTypeId && isEquivalentParameters ( testParameters , test_file ?. ConfigurationParameters , "ParameterNumber" ) ) { file . combinedDevices = createOrUpdateArray ( file . combinedDevices , { ProductId : test_file . ProductId , ProductTypeId : test_file . ProductTypeId , Id : test_file . Id , Brand : test_file . Brand , Identifier : test_file . Identifier } ) ; delete test_file . Identifier ; delete test_file . ProductId ; file . SupportedCommandClasses = keepLongest ( file . SupportedCommandClasses , test_file . SupportedCommandClasses ) ; file . AssociationGroups = keepLongest ( file . AssociationGroups , test_file . AssociationGroups ) ; file . Documents = keepLongest ( file . Documents , test_file . Documents ) ; file . Texts = keepLongest ( file . Texts , test_file . Texts ) ; file . Features = keepLongest ( file . Features , test_file . Features ) ; } else if ( test_file . Identifier === testIdentifier && testIdentifier !== "Unknown" && testIdentifier . length > 3 && isEquivalentParameters ( testParameters , test_file ?. ConfigurationParameters , "ParameterNumber" ) ) { file . combinedDevices = createOrUpdateArray ( file . combinedDevices , { ProductId : test_file . ProductId , ProductTypeId : test_file . ProductTypeId , Id : test_file . Id , Brand : test_file . Brand , Identifier : test_file . Identifier } ) ; delete test_file . Identifier ; delete test_file . ProductId ; if ( bothZwavePlus ( file . SupportedCommandClasses , test_file . SupportedCommandClasses ) ) { file . SupportedCommandClasses = keepLongest ( file . SupportedCommandClasses , test_file . SupportedCommandClasses ) ; } else { file . SupportedCommandClasses = [ ] ; } file . AssociationGroups = keepLongest ( file . AssociationGroups , test_file . AssociationGroups ) ; file . Documents = keepLongest ( file . Documents , test_file . Documents ) ; file . Texts = keepLongest ( file . Texts , test_file . Texts ) ; file . Features = keepLongest ( file . Features , test_file . Features ) ; } else if ( test_file . ProductId === file . ProductId && test_file . ProductTypeId === file . ProductTypeId && isEquivalentParameters ( testParameters , test_file ?. ConfigurationParameters , "ParameterNumber" ) == false ) { console . log ( `WARNING - Detected possible firmware parameter change ${ file . Identifer } -- ${ file . Id } and ${ test_file . Id }` ) ; } else if ( test_file . Identifier === testIdentifier && isEquivalentParameters ( testParameters , test_file ?. ConfigurationParameters , "ParameterNumber" ) === false ) { test_file . Identifier = test_file . OriginalIdentifier ; } } } } function keepLongest ( current_group , test_group ) { if ( current_group . length >= test_group . length ) { return current_group ; } else { return test_group ; } } function bothZwavePlus ( current_group , test_group ) { for ( const z of current_group ) { for ( const class2 of test_group ) { if ( ( z . Identifier . includes ( "ZWAVEPLUS" ) || z . Identifier . includes ( "ASSOCIATION_GRP_INFO" ) ) && ( class2 . Identifier . includes ( "ZWAVEPLUS" ) || class2 . Identifier . includes ( "ASSOCIATION_GRP_INFO" ) ) ) { return true ; } } } return false ; } return json ; } /***
 * Combine zWave Alliance Device Files
 */ function sanitizeFields ( json ) { for ( const file of json ) { if ( file . ProductId ) { file . Identifier = file . Identifier ? sanitizeString ( file . Identifier ) : "" ; file . Brand = file . Brand ? sanitizeString ( file . Brand ) : "" ; } if ( file . AssociationGroups ) { for ( const assoc of file . AssociationGroups ) { assoc . Description = assoc . Description ? sanitizeString ( assoc . Description ) : "" ; assoc . group_name = assoc . group_name ? sanitizeString ( assoc . group_name ) : "" ; } } if ( file . ConfigurationParameters ) { for ( const param of file . ConfigurationParameters ) { param . Name = param . Name ? sanitizeString ( param . Name ) : "" ; param . Name = param . Name ? param . Name . replace ( /\.\"/g , '"' ) : "" ; param . Name = param . Name ? param . Name . replace ( /[\,\.\:]$/ , '"' ) : "" ; param . Name = param . Name ? param . Name . replace ( /\:\"/g , '"' ) : "" ; param . Description = param . Description ? sanitizeString ( param . Description ) : "" ; if ( param . ConfigurationParameterValues ) { for ( const value of param . ConfigurationParameterValues ) { value . Description = value . Description ? sanitizeString ( value . Description ) : "" ; value . Description = value . Description ? value . Description . replace ( /[\,\.\:]$/ , '"' ) : "" ; } } } } if ( file . Texts ) { for ( const text of file . Texts ) { text . description = text . description ? sanitizeString ( text . description ) : "" ; text . value = text . value ? sanitizeString ( text . value ) : "" ; } } } return json ; } param product async function parseZWAProduct ( product , manufacturerId , manufacturer ) { const productLabel = product . Identifier ; const productName = product . Name . replace ( productLabel , "" ) ; let manufacturerIdHex = product . ManufacturerId . replace ( /^0x/ , "" ) ; manufacturerIdHex = formatId ( manufacturerIdHex ) ; /*************************************
     *	Load the device configurations   *
     *************************************/ let deviceConfigs ; for ( const device of product . combinedDevices ) { deviceConfigs = configManager . getIndex ( ) ?. filter ( ( f ) => f . manufacturerId === manufacturerIdHex && f . productType === device . ProductTypeId && f . productId === device . ProductId ) ?? [ ] ; if ( deviceConfigs ) { break ; } } const latestConfig = getLatestConfigVersion ( deviceConfigs ) ; let fileNameRelative ; if ( latestConfig ?. filename ) { fileNameRelative = latestConfig ?. filename ; } else { fileNameRelative = latestConfig ?. filename ?? `${ manufacturerIdHex }/${ labelToFilename ( productLabel ) }.json` ; } const fileNameAbsolute = path . join ( processedDir , fileNameRelative ) ; let existingDevice ; try { if ( await fs . pathExists ( fileNameAbsolute ) ) { existingDevice = JSONC . parse ( await fs . readFile ( fileNameAbsolute , "utf8" ) ) ; } } catch ( e ) { console . log ( `Error processing: ${ fileNameAbsolute } - ${ getErrorMessage ( e , true ) }` ) ; } /********************************
     *    Build the device lists    *
     ********************************/ const devices = existingDevice ?. devices ?? [ ] ; for ( const dev of product . combinedDevices ) { for ( const eDevice of devices ) { if ( eDevice . productType == dev . ProductTypeId && eDevice . productId == dev . ProductId ) { eDevice . zwaveAllianceId = dev . Id ; } } if ( ! devices . some ( ( d ) => d . productType === dev . ProductTypeId && d . productId === dev . ProductId ) ) { devices . push ( { productType : dev . ProductTypeId , productId : dev . ProductId , zwaveAllianceId : dev . Id } ) ; } } /***************************************
     *   Setup the initial configuration   *
     ***************************************/ const inclusion = product ?. Texts ?. find ( ( document ) => document . Type === 1 ) ?. value ; const exclusion = product ?. Texts ?. find ( ( document ) => document . Type === 2 ) ?. value ; const reset = product ?. Texts ?. find ( ( document ) => document . Type === 5 ) ?. value ; let manual = product ?. Documents ?. find ( ( document ) => document . Type === 1 ) ?. value ; const website_root = "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=" ; if ( manual ) { manual = manual . replace ( / /g , "%20" ) ; manual = website_root . concat ( manual ) ; } const newConfig = { manufacturer , manufacturerId : manufacturerIdHex , label : productLabel , description : existingDevice ?. description ?? productName , devices : devices , firmwareVersion : { min : existingDevice ?. firmwareVersion . min ?? "0.0" , max : existingDevice ?. firmwareVersion . max ?? "255.255" } , associations : existingDevice ?. associations ?? { } , paramInformation : existingDevice ?. paramInformation ?? [ ] , compat : existingDevice ?. compat } ; if ( inclusion || exclusion || reset || manual ) { newConfig . metadata = { inclusion : inclusion , exclusion : exclusion , reset : reset , manual : manual } ; } /***************************
     *     Clean up values     *
     ***************************/ newConfig . description = sanitizeString ( newConfig . description ) ; /**********************
     *     Parameters     *
     **********************/ const parameters = product . ConfigurationParameters ; for ( const param of parameters ) { const found = newConfig . paramInformation . find ( ( p ) => p [ "#" ] === param . ParameterNumber . toString ( ) ) ; const parsedParam = found ?? { } ; if ( parsedParam . $import ) { continue ; } if ( newConfig . paramInformation . some ( ( p ) => p [ "#" ] ?. startsWith ( `${ param . ParameterNumber }[` ) ) ) { continue ; } parsedParam [ "#" ] = param . ParameterNumber . toString ( ) ; parsedParam . label = param . Name || parsedParam . label ; parsedParam . label = normalizeLabel ( parsedParam . label ) ; parsedParam . description = param . ConfigurationParameterValues . length > 1 ? param . Description : param . ConfigurationParameterValues [ 0 ] . Description ; parsedParam . description = normalizeDescription ( parsedParam . description ) ; parsedParam . valueSize = updateNumberOrDefault ( param . Size , parsedParam . valueSize , 1 ) ; parsedParam . minValue = param . minValue ; parsedParam . maxValue = param . maxValue ; if ( param . flagReadOnly === true ) { parsedParam . readOnly = true ; } else if ( param . Description . toLowerCase ( ) . includes ( "write" ) ) { parsedParam . writeOnly = true ; } parsedParam . allowManualEntry = ! parsedParam . readOnly && param . ConfigurationParameterValues . length <= 1 ; parsedParam . defaultValue = updateNumberOrDefault ( param . DefaultValue , parsedParam . value , parsedParam . minValue ) ; if ( /hours?/i . test ( parsedParam . description ) ) { parsedParam . unit = "hours" ; } else if ( /minutes?/i . test ( parsedParam . description ) ) { parsedParam . unit = "minutes" ; } else if ( /seconds?/i . test ( parsedParam . description ) ) { parsedParam . unit = "seconds" ; } else if ( /percent(age)?/i . test ( parsedParam . description ) ) { parsedParam . unit = "%" ; } else if ( /centigrade|celsius/i . test ( parsedParam . description ) ) { parsedParam . unit = "°C" ; } else if ( /fahrenheit/i . test ( parsedParam . description ) ) { parsedParam . unit = "°F" ; } parsedParam . minValue = parsedParam . minValue <= parsedParam . defaultValue ? parsedParam . minValue : parsedParam . defaultValue ; parsedParam . maxValue = parsedParam . maxValue >= parsedParam . defaultValue ? parsedParam . maxValue : parsedParam . defaultValue ; if ( parsedParam . minValue >= 0 ) { parsedParam . unsigned = true ; } else { delete parsedParam . unsigned ; } if ( typeof parsedParam . description !== "string" ) { parsedParam . description = "" ; } if ( parsedParam . allowManualEntry !== true || ( parsedParam . minValue === 0 && parsedParam . maxValue === 0 ) ) { parsedParam . options = [ ] ; for ( const item of param . ConfigurationParameterValues ) { if ( item . From === item . To ) { const opt = { label : normalizeDescription ( item . Description ) , value : item . To } ; parsedParam . options . push ( opt ) ; parsedParam . minValue = Math . min ( parsedParam . minValue , item . From ) ; parsedParam . maxValue = Math . max ( parsedParam . maxValue , item . To ) ; } else { parsedParam . allowManualEntry = true ; parsedParam . minValue = Math . min ( parsedParam . minValue , item . From ) ; parsedParam . maxValue = Math . max ( parsedParam . maxValue , item . To ) ; } } } if ( ! found ) newConfig . paramInformation . push ( parsedParam ) ; } /********************************
     *        Associations		    *
     ********************************/ let zwavePlus = false ; zwavePlus = product ?. SupportedCommandClasses ?. find ( ( document ) => document . Identifier . includes ( "ZWAVEPLUS" ) ) ? true : zwavePlus ; zwavePlus = product ?. SupportedCommandClasses ?. find ( ( document ) => document . Identifier . includes ( "ASSOCIATION_GRP_INFO" ) ) ? true : zwavePlus ; zwavePlus = product ?. AssociationGroups ?. find ( ( document ) => document . Description . includes ( "Z-Wave Plus" ) ) ? true : zwavePlus ; zwavePlus = existingDevice ?. supportsZWavePlus ? true : zwavePlus ; const newAssociations = newConfig . associations || { } ; let addCompat = false ; for ( const ass of product . AssociationGroups ) { let label = ass . group_name . length > 0 ? ass . group_name : `Group ${ ass . GroupNumber }` ; const maxNodes = ass . MaximumNodes ; const groupName = ass . group_name . toLowerCase ( ) ; const description = ass . Description . toLowerCase ( ) ; let lifeline = false ; if ( groupName . includes ( "lifeline" ) || description . includes ( "lifeline" ) ) { lifeline = true ; if ( ass . GroupNumber !== 1 ) { zwavePlus = false ; } } if ( groupName . includes ( "double" ) || description . includes ( "double" ) ) { label = "Double Tap" ; lifeline = true ; zwavePlus = false ; addCompat = true ; newConfig . compat ??= { } ; newConfig . compat . treatBasicSetAsEvent = true ; } newAssociations [ ass . GroupNumber ] = { label : label , maxNodes : maxNodes } ; if ( lifeline ) { newAssociations [ ass . GroupNumber ] . isLifeline = true ; } } if ( Object . keys ( newConfig . associations ) . length !== 0 && addCompat ) { newConfig . associations = newAssociations ; } else if ( Object . keys ( newConfig . associations ) . length === 0 && zwavePlus === false ) { newConfig . associations = newAssociations ; } /*************************************
     *   Write the configuration file    *
     *************************************/ const manufacturerDir = path . join ( processedDir , manufacturerIdHex ) ; await fs . ensureDir ( manufacturerDir ) ; const output = JSONC . stringify ( normalizeConfig ( newConfig ) , null , "\t" ) + "\n" ; await fs . writeFile ( fileNameAbsolute , output , "utf8" ) ; } async function maintenanceParse ( ) { const zwaData = [ ] ; await fs . ensureDir ( zwaTempDir ) ; const zwaFiles = await enumFilesRecursive ( zwaTempDir , ( file ) => file . endsWith ( ".json" ) ) ; for ( const file of zwaFiles ) { try { zwaData . push ( await fs . readJSON ( file , { encoding : "utf8" } ) ) ; } catch { await fs . unlink ( file ) ; } } const configFiles = await enumFilesRecursive ( processedDir , ( file ) => file . endsWith ( ".json" ) ) ; for ( const file of configFiles ) { const j = await fs . readFile ( file , "utf8" ) ; let jsonData ; try { jsonData = JSONC . parse ( j ) ; } catch ( e ) { console . log ( `Error processing: ${ file } - ${ getErrorMessage ( e , true ) }` ) ; } const includedZwaFiles = [ ] ; try { for ( const device of jsonData . devices ) { if ( isArray ( device . zwaveAllianceId ) ) { includedZwaFiles . push ( ... device . zwaveAllianceId ) ; } else if ( device . zwaveAllianceId ) { includedZwaFiles . push ( device . zwaveAllianceId ) ; } } } catch ( e ) { console . log ( `Error iterating: ${ file } - ${ getErrorMessage ( e , true ) }` ) ; } includedZwaFiles . sort ( function ( a , b ) { return a - b ; } ) ; for ( const referenceDevice of includedZwaFiles ) { for ( const zwafile of zwaData ) { if ( zwafile . Id === referenceDevice ) { let manual = zwafile ?. Documents ?. find ( ( document ) => document . Type === 1 ) ?. value ; const website_root = "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=" ; if ( manual ) { manual = manual . replace ( / /g , "%20" ) ; manual = website_root . concat ( manual ) ; if ( jsonData . metadata ) { jsonData . metadata . manual = manual ; break ; } else { jsonData . metadata = { } ; jsonData . metadata . manual = manual ; break ; } } } } } if ( jsonData . metadata ) { /*************************************
             *   Write the configuration file    *
             *************************************/ const output = JSONC . stringify ( normalizeConfig ( jsonData ) , null , "\t" ) + "\n" ; await fs . writeFile ( file , output , "utf8" ) ; } } } /**
 * Retrieve ZWA device IDs, either the highest (most recent) device ID or all device IDs for the specified manufacturer
 * Note: ZWA's search uses different manufacturer IDs than devices
 */ async function retrieveZWADeviceIds ( highestDeviceOnly = true , manufacturer = [ - 1 ] ) { const deviceIdsSet = new Set < number > ( ) ; for ( const manu of manufacturer ) { let page = 1 ; let currentUrl = `https://products.z-wavealliance.org/search/DoAdvancedSearch?productName=&productIdentifier=&productDescription=&category=-1&brand=${ manu }&regionId=-1&order=&page=${ page }` ; const firstPage = ( await axios ( { url : currentUrl } ) ) . data ; for ( const i of firstPage . match ( /(?<=productId=).*?(?=[\&\"])/g ) ) { deviceIdsSet . add ( i ) ; } const pageNumbers = firstPage . match ( /(?<=page=\d+">).*?(?=\<)/g ) ? firstPage . match ( /(?<=page=\d+">).*?(?=\<)/g ) : [ 1 ] ; const lastPage = Math . max ( ... pageNumbers ) ; process . stdout . write ( `Processing Page 1 of ${ lastPage }...` ) ; process . stdout . write ( "\r\x1b[K" ) ; if ( ! highestDeviceOnly ) { page ++ ; while ( page <= lastPage ) { process . stdout . write ( `Processing Page ${ page } of ${ lastPage }...` ) ; currentUrl = `https://products.z-wavealliance.org/search/DoAdvancedSearch?productName=&productIdentifier=&productDescription=&category=-1&brand=${ manu }&regionId=-1&order=&page=${ page }` ; const nextPage = ( await axios ( { url : currentUrl } ) ) . data ; const nextPageIds = nextPage . match ( /(?<=productId=).*?(?=[\&\"])/g ) ; for ( const i of nextPageIds ) { deviceIdsSet . add ( i ) ; } page ++ ; process . stdout . write ( "\r\x1b[K" ) ; } } } if ( highestDeviceOnly ) { const deviceIds = [ ... deviceIdsSet ] ; deviceIds . sort ( function ( a , b ) { return b - a ; } ) ; console . log ( `Highest Device Found: ${ deviceIds [ 0 ] }` ) ; return [ deviceIds [ 0 ] ] ; } else { const deviceIds = [ ... deviceIdsSet ] ; console . log ( `Identified ${ deviceIds . length } device files` ) ; return deviceIds ; } } param IDs async function downloadDevicesZWA ( IDs ) { await fs . ensureDir ( zwaTempDir ) ; for ( let i = 0 ; i < IDs . length ; i ++ ) { process . stdout . write ( `Fetching device config ${ i + 1 } of ${ IDs . length }...` ) ; const content = await fetchDeviceZWA ( IDs [ i ] ) ; await fs . writeFile ( path . join ( zwaTempDir , `${ IDs [ i ] }.json` ) , content , "utf8" ) ; process . stdout . write ( "\r\x1b[K" ) ; } console . log ( "done!" ) ; } param IDs async function downloadDevicesOH ( IDs ? ) { if ( ! isArray ( IDs ) || ! IDs . length ) { process . stdout . write ( "Fetching database IDs..." ) ; IDs = await fetchIDsOH ( ) ; process . stdout . write ( "\r\x1b[K" ) ; } await fs . ensureDir ( ohTempDir ) ; for ( let i = 0 ; i < IDs . length ; i ++ ) { process . stdout . write ( `Fetching device config ${ i + 1 } of ${ IDs . length }...` ) ; const content = await fetchDeviceOH ( IDs [ i ] ) ; await fs . writeFile ( path . join ( ohTempDir , `${ IDs [ i ] }.json` ) , content , "utf8" ) ; process . stdout . write ( "\r\x1b[K" ) ; } console . log ( "done!" ) ; } /** Downloads all manufacturer information from the OpenSmartHouse DB */ async function downloadManufacturersOH ( ) { process . stdout . write ( "Fetching manufacturers..." ) ; const data = ( await axios ( { url : ohUrlManufacturers } ) ) . data ; process . stdout . write ( "\r\x1b[K" ) ; const manufacturers = composeObject ( data . manufacturers . data . map ( ( { id , label } ) => [ label . replace ( "</a>" , "" ) . replace ( /&quot;/g , `"` ) . replace ( /&amp;/g , "&" ) . trim ( ) , formatId ( id ) , ] ) ) ; await fs . ensureDir ( ohTempDir ) ; await fs . writeFile ( importedManufacturersPath , stringify ( manufacturers , "\t" ) , "utf8" ) ; console . log ( "done!" ) ; } /** Ensures an input file is valid */ function assertValid ( json ) { ok ( isObject ( json . manufacturer ) && typeof json . manufacturer . reference === "number" && typeof json . manufacturer . label === "string" ) ; ok ( typeof json . description === "string" ) ; ok ( typeof json . label === "string" ) ; ok ( typeof json . device_ref === "string" ) ; ok ( typeof json . version_min === "string" ) ; ok ( typeof json . version_max === "string" ) ; } /** Removes unnecessary whitespace from imported text */ function sanitizeText ( text ) { return text ? text . trim ( ) . replace ( /[\t\r\n]+/g , " " ) : undefined ; } /** Tries to coerce the input value into an integer */ function sanitizeNumber ( value ) { if ( typeof value === "number" ) return value ; if ( isNullishOrEmptyString ( value ) ) return undefined ; let ret = Number ( value ) ; if ( Number . isNaN ( ret ) ) { value = value . replace ( /[^0-9-\.\,]/g , "" ) ; ret = Number ( value ) ; } return ret ; } /** Converts a device label to a valid filename */ function labelToFilename ( label ) { return label . trim ( ) . replace ( /[^a-zA-Z0-9\-_]+/g , "_" ) . replace ( /^_/ , "" ) . replace ( /_$/ , "" ) . toLowerCase ( ) ; } /** Parses a downloaded config file into something we understand */ async function parseOHConfigFile ( filename ) { const content = await fs . readFile ( filename , "utf8" ) ; const json = JSON . parse ( content ) ; assertValid ( json ) ; const ret = { manufacturer : json . manufacturer . label , manufacturerId : formatId ( json . manufacturer . reference ) , label : sanitizeText ( json . label ) , description : sanitizeText ( json . description ) , devices : json . device_ref . split ( "," ) . filter ( Boolean ) . map ( ( ref ) => { const [ productType , productId ] = ref . trim ( ) . split ( ":" ) . map ( ( str ) => formatId ( str ) ) ; return { productType , productId } ; } ) , firmwareVersion : { min : json . version_min . replace ( /000/g , "0" ) , max : json . version_max } } ; try { const supportsZWavePlus = ! ! json . endpoints ?. find ( ( ep ) => ep . number === "0" ) ?. commandClasses ?. find ( ( cc ) => cc . commandclass . cmdclass_id === 94 ) ; if ( ! supportsZWavePlus ) { if ( json . associations ?. length ) { ret . associations = { } ; for ( const assoc of json . associations ) { const sanitizedDescription = sanitizeText ( assoc . description ) ; ret . associations [ assoc . group_id ] = { label : sanitizeText ( assoc . label ) , ... ( sanitizedDescription ? { description : sanitizedDescription } : undefined ) , maxNodes : parseInt ( assoc . max_nodes ) , isLifeline : assoc . controller === "1" ? true : undefined } ; } } } else { } } catch ( e ) { console . error ( filename ) ; process . exit ( 1 ) ; } if ( json . parameters ?. length ) { ret . paramInformation = [ ] ; for ( const param of json . parameters ) { let key = param . param_id . toString ( ) ; if ( param . bitmask !== "0" && param . bitmask !== 0 ) { const bitmask = parseInt ( param . bitmask ) ; key += `[${ num2hex ( bitmask ) }]` ; } const sanitizedDescription = sanitizeText ( param . description ) ; const sanitizedUnits = sanitizeText ( param . units ) ; const paramInfo = { "#" : key , label : sanitizeText ( param . label ) , ... ( sanitizedDescription ? { description : sanitizedDescription } : undefined ) , ... ( sanitizedUnits ? { unit : sanitizedUnits } : undefined ) , valueSize : parseInt ( param . size , 10 ) , minValue : parseInt ( param . minimum , 10 ) , maxValue : parseInt ( param . maximum , 10 ) , defaultValue : parseInt ( param . default , 10 ) , readOnly : param . read_only === "1" ? true : undefined , writeOnly : param . write_only === "1" ? true : undefined , allowManualEntry : param . limit_options === "1" } ; if ( param . options ?. length ) { paramInfo . options = param . options . map ( ( opt ) => ( { label : sanitizeText ( opt . label ) , value : parseInt ( opt . value , 10 ) } ) ) ; } ret . paramInformation . push ( paramInfo ) ; } } return ret ; } /** Translates all downloaded config files */ async function importConfigFilesOH ( ) { const configFiles = ( await fs . readdir ( ohTempDir ) ) . filter ( ( file ) => file . endsWith ( ".json" ) && ! file . startsWith ( "_" ) && file !== "manufacturers.json" ) ; for ( const file of configFiles ) { const inPath = path . join ( ohTempDir , file ) ; let parsed ; try { parsed = await parseOHConfigFile ( inPath ) ; if ( ! parsed . manufacturerId ) { console . error ( `${ file } has no manufacturer ID!` ) ; } if ( ! parsed . label ) { console . error ( `${ file } has no label, ignoring it!` ) ; continue ; } } catch ( e ) { if ( e instanceof AssertionError ) { console . error ( `${ file } is not valid, ignoring!` ) ; continue ; } throw e ; } let outFilename = path . join ( processedDir , parsed . manufacturerId , labelToFilename ( parsed . label ) ) ; if ( parsed . firmwareVersion . min !== "0.0" || parsed . firmwareVersion . max !== "255.255" ) { outFilename += `_${ parsed . firmwareVersion . min }` ; if ( parsed . firmwareVersion . max !== "255.255" ) { outFilename += `-${ parsed . firmwareVersion . max }` ; } } outFilename += ".json" ; await fs . ensureDir ( path . dirname ( outFilename ) ) ; const output = stringify ( parsed , "\t" ) + "\n" ; await fs . writeFile ( outFilename , output , "utf8" ) ; } } /****************************************************************************
 *                   Normalize Identifier function                          *
 * Strips out common model number variations representing different         *
 * jurisdicitons, which typically share firmware and parameter settings     *
 * 																			*
 * Note: Returns an array of the new identifier, and the original           *
 ****************************************************************************/ function normalizeIdentifier ( originalIdentifier ) { let newIdentifier = originalIdentifier ; const country_codes = [ "(US)" , "us" , "US" , "(EU)" , "eu" , "EU" , "(RU)" , "ru" , "RU" , "(AU)" , "au" , "AU" , "(CM)" , "cm" , "CM" , ] ; for ( const code of country_codes ) { const regex = new RegExp ( code , "g" ) ; newIdentifier = newIdentifier . replace ( regex , "" ) ; } const suffixToRemove = [ "-1" , "-2" , "-3" , "-4" , "a" , "A" , "b" , "B" , "c" , "C" , "d" , "D" , "e" , "i" , "I" , "j" , "J" , ] ; for ( const suffix of suffixToRemove ) { if ( newIdentifier . slice ( - suffix . length ) == suffix ) { newIdentifier = newIdentifier . slice ( 0 , - suffix . length ) ; break ; } } const prohibitedEndChars = [ "-" , "." , "_" , "," , " " ] ; for ( const endChar of prohibitedEndChars ) { if ( newIdentifier . slice ( - 1 ) === endChar ) { newIdentifier = newIdentifier . slice ( 0 , - 1 ) ; break ; } } newIdentifier = newIdentifier . trim ( ) ; newIdentifier = newIdentifier . toLocaleUpperCase ( ) ; return [ newIdentifier , originalIdentifier ] ; } /****************************************************************************
 *                   Normalize label function                               *
 * Capitilze each word in a label        							        *
 * 																			*
 ****************************************************************************/ function normalizeLabel ( originalString ) { originalString = sanitizeString ( originalString ) ; originalString = originalString . replace ( /\n/g , " " ) ; originalString = originalString . replace ( /\\"/g , "" ) ; let splitStr = originalString . toLowerCase ( ) . split ( " " ) ; for ( let i = 0 ; i < splitStr . length ; i ++ ) { splitStr [ i ] = splitStr [ i ] . charAt ( 0 ) . toUpperCase ( ) + splitStr [ i ] . substring ( 1 ) ; } originalString = splitStr . join ( " " ) ; splitStr = originalString . split ( "/" ) ; for ( let i = 0 ; i < splitStr . length ; i ++ ) { splitStr [ i ] = splitStr [ i ] . charAt ( 0 ) . toUpperCase ( ) + splitStr [ i ] . substring ( 1 ) ; } originalString = splitStr . join ( "/" ) ; originalString = originalString . replace ( / Led /g , " LED " ) ; originalString = originalString . replace ( / Rgb /g , " RGB " ) ; originalString = originalString . replace ( / Pir /g , " PIR " ) ; originalString = originalString . replace ( /Z-wave/g , "Z-Wave" ) ; originalString = originalString . replace ( /basic set/g , "Basic Set" ) ; originalString = originalString . replace ( /multi-level switch/g , "Multi-Level Switch" ) ; originalString = originalString . replace ( /Multi-level/g , "Multi-Level" ) ; originalString = originalString . replace ( / Of /g , " of " ) ; originalString = originalString . replace ( / To /g , " to " ) ; originalString = originalString . replace ( / A /g , " a " ) ; originalString = originalString . replace ( / An /g , " an " ) ; originalString = originalString . replace ( / Is /g , " is " ) ; originalString = originalString . replace ( / In /g , " in " ) ; const prohibitedEndChars = [ "-" , "." , "_" , "," , " " ] ; for ( const endChar of prohibitedEndChars ) { if ( originalString . slice ( - 1 ) === endChar ) { originalString = originalString . slice ( 0 , - 1 ) ; break ; } } for ( const endChar of prohibitedEndChars ) { if ( originalString . slice ( 0 ) === endChar ) { originalString = originalString . slice ( 1 , 0 ) ; break ; } } originalString = originalString . trim ( ) ; return originalString ; } /****************************************************************************
 *                   Normalize description                                  *
 * Capitilze each word in a description          					        *
 * 																			*
 ****************************************************************************/ function normalizeDescription ( originalString ) { originalString = sanitizeString ( originalString ) . toLocaleLowerCase ( ) . replace ( /\n/g , " " ) . replace ( /\\"/g , "" ) . replace ( / led /g , " LED " ) . replace ( / rgb /g , " RGB " ) . replace ( / pir /g , " PIR " ) . replace ( /basic set/g , "Basic Set" ) . replace ( /multi-level/g , "Multi-Level" ) . replace ( /z-?wave/g , "Z-Wave" ) ; originalString = originalString . charAt ( 0 ) . toUpperCase ( ) + originalString . slice ( 1 ) ; originalString = originalString . replace ( /multi-level switch/g , "Multi-Level Switch" ) ; originalString = originalString . replace ( /multi-level/g , "Multi-Level" ) ; originalString = originalString . replace ( /[._, -]+$/ , "" ) ; originalString = originalString . replace ( /^[._, -]+/ , "" ) ; return originalString ; } /****************************************************************************
 *                   Sanitize String function                             *
 * Strips out common mistakes in strings							        *
 * 																			*
 ****************************************************************************/ function sanitizeString ( originalString ) { return originalString . replace ( /\r\n/g , "\n" ) . replace ( /\r/g , "\n" ) . replace ( /\n\n\n\n/g , "\n\n" ) . replace ( /\t/g , " " ) . replace ( /\"\"/g , '"' ) . replace ( / {2,}/g , " " ) . replace ( /\,s*$/g , "" ) . replace ( /\„s*$/g , "" ) . replace ( /\.s*$/g , "" ) . replace ( /\:s*$/g , "" ) . trim ( ) ; } /****************************************************************************
 *                   Parameter Comparison function                          *
 * Compare two sets of parameters and return true if the numbers match      *
 * 																			*
 * Note: Accepts as a string the name of the parameter key in the object    *
 ****************************************************************************/ function isEquivalentParameters ( tested_device = [ ] , compared_device = [ ] , parameterKey ) { const testParameters = new Set ( ) ; for ( const tp of tested_device ) { const temp = tp [ parameterKey ] ; testParameters . add ( temp ) ; } const compareParameters = new Set ( ) ; for ( const cp of compared_device ) { const temp = cp [ parameterKey ] ; compareParameters . add ( temp ) ; } const setDifference = new Set ( [ ... compareParameters ] . filter ( ( x ) => ! testParameters . has ( x ) ) ) ; return setDifference . size === 0 ; } /****************************************************************************
 *                 Create array, or update array if it exists,              *
 * 				   append ZWA Ids										    *
 ****************************************************************************/ function createOrUpdateArray ( potentialArray , update ) { potentialArray = potentialArray ?? [ ] ; const newArray = potentialArray ; for ( const arr of newArray ) { if ( ( arr . ProductId === update . ProductId && arr . ProductTypeId === update . ProductTypeId ) || ( arr . ProductId === formatId ( update . ProductId ) && arr . ProductTypeId === formatId ( update . ProductTypeId ) ) ) { if ( Array . isArray ( arr . Id ) ) { arr . Id . push ( update . Id ) ; } else { const temp = arr . Id ; arr . Id = [ ] ; arr . Id . push ( temp ) ; arr . Id . push ( update . Id ) ; } return newArray ; } } newArray . push ( update ) ; return newArray ; } param configs function getLatestConfigVersion ( configs ) { configs . sort ( ( a , b ) => { const vA = padVersion ( a . firmwareVersion . max ) ; const vB = padVersion ( b . firmwareVersion . max ) ; return compare ( vA , vB ) ; } ) ; return configs [ configs . length - 1 ] ; } /** Changes the manufacturer names in all device config files to match manufacturers.json */ async function updateManufacturerNames ( ) { const configFiles = await enumFilesRecursive ( processedDir , ( file ) => file . endsWith ( ".json" ) && ! file . endsWith ( "index.json" ) ) ; await configManager . loadManufacturers ( ) ; for ( const file of configFiles ) { let fileContents = await fs . readFile ( file , "utf8" ) ; const id = parseInt ( /"manufacturerId"\: "0x([0-9a-fA-F]+)"/ . exec ( fileContents ) ! [ 1 ] , 16 ) ; const name = configManager . lookupManufacturer ( id ) ; const oldName = /"manufacturer"\: "([^\"]+)"/ . exec ( fileContents ) ! [ 1 ] ; if ( oldName && name && name !== oldName ) { fileContents = fileContents . replace ( `// ${ oldName } ` , `// ${ name } ` ) ; fileContents = fileContents . replace ( `"manufacturer": "${ oldName }"` , `"manufacturer": "${ name }"` ) ; await fs . writeFile ( file , fileContents , "utf8" ) ; } } } void ( async ( ) => { if ( program . clean ) { await cleanTmpDirectory ( ) ; } else { if ( program . source . includes ( "ozw" ) ) { if ( program . download ) { await downloadOZWConfig ( ) ; await extractConfigFromTar ( ) ; } if ( program . manufacturers || program . devices ) await parseOZWConfig ( ) ; } if ( program . source . includes ( "zwa" ) ) { if ( program . manufacturer_folder ) { const deviceIds = await retrieveZWADeviceIds ( false , program . manufacturer_folder ?. map ( ( manu ) => parseInt ( manu as any ) ) . filter ( ( num ) => ! Number . isNaN ( num ) ) ) ; await downloadDevicesZWA ( deviceIds ) ; } else if ( program . download && program . ids ) { await downloadDevicesZWA ( program . ids ?. map ( ( id ) => parseInt ( id as any ) ) . filter ( ( num ) => ! Number . isNaN ( num ) ) ) ; } if ( program . manufacturers || program . devices ) await parseZWAFiles ( ) ; } if ( program . source . includes ( "oh" ) ) { if ( program . download ) { await downloadManufacturersOH ( ) ; await downloadDevicesOH ( program . ids ?. map ( ( id ) => parseInt ( id as any ) ) . filter ( ( num ) => ! Number . isNaN ( num ) ) ) ; } if ( program . manufacturers ) { await updateManufacturerNames ( ) ; } if ( program . devices ) { await importConfigFilesOH ( ) ; } } if ( program . parse ) { await maintenanceParse ( ) ; } } } ) ( ) ;