inventoresed
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
1,128 lines (1,034 loc) • 30.2 kB
text/typescript
import {
isZWaveError,
ZWaveError,
ZWaveErrorCodes,
ZWaveLogContainer,
} from "@zwave-js/core";
import { getErrorMessage, JSONObject, num2hex } from "@zwave-js/shared";
import { isObject } from "alcalzone-shared/typeguards";
import { pathExists, readFile } from "fs-extra";
import JSON5 from "json5";
import path from "path";
import {
BasicDeviceClass,
BasicDeviceClassMap,
GenericDeviceClass,
GenericDeviceClassMap,
getDefaultGenericDeviceClass,
getDefaultSpecificDeviceClass,
SpecificDeviceClass,
} from "./DeviceClasses";
import {
ConditionalDeviceConfig,
DeviceConfig,
DeviceConfigIndex,
FulltextDeviceConfigIndex,
generatePriorityDeviceIndex,
getDevicesPaths,
loadDeviceIndexInternal,
loadFulltextDeviceIndexInternal,
} from "./devices/DeviceConfig";
import {
IndicatorMap,
IndicatorPropertiesMap,
IndicatorProperty,
} from "./Indicators";
import { ConfigLogger } from "./Logger";
import {
loadManufacturersInternal,
ManufacturersMap,
saveManufacturersInternal,
} from "./Manufacturers";
import { getDefaultMeterScale, Meter, MeterMap, MeterScale } from "./Meters";
import { Notification, NotificationMap } from "./Notifications";
import {
getDefaultScale,
NamedScalesGroupMap,
Scale,
ScaleGroup,
} from "./Scales";
import { SensorType, SensorTypeMap } from "./SensorTypes";
import {
configDir,
externalConfigDir,
getDeviceEntryPredicate,
getEmbeddedConfigVersion,
syncExternalConfigDir,
} from "./utils";
import { hexKeyRegexNDigits, throwInvalidConfig } from "./utils_safe";
export interface ConfigManagerOptions {
logContainer?: ZWaveLogContainer;
deviceConfigPriorityDir?: string;
}
export class ConfigManager {
public constructor(options: ConfigManagerOptions = {}) {
this.logger = new ConfigLogger(
options.logContainer ?? new ZWaveLogContainer({ enabled: false }),
);
this.deviceConfigPriorityDir = options.deviceConfigPriorityDir;
this._configVersion =
// eslint-disable-next-line @typescript-eslint/no-var-requires
require("@zwave-js/config/package.json").version;
}
private _configVersion: string;
public get configVersion(): string {
return this._configVersion;
}
private logger: ConfigLogger;
private _indicators: IndicatorMap | undefined;
public get indicators(): IndicatorMap {
if (!this._indicators) {
throw new ZWaveError(
"The config has not been loaded yet!",
ZWaveErrorCodes.Driver_NotReady,
);
}
return this._indicators;
}
private _indicatorProperties: IndicatorPropertiesMap | undefined;
public get indicatorProperties(): IndicatorPropertiesMap {
if (!this._indicatorProperties) {
throw new ZWaveError(
"The config has not been loaded yet!",
ZWaveErrorCodes.Driver_NotReady,
);
}
return this._indicatorProperties;
}
private _manufacturers: ManufacturersMap | undefined;
public get manufacturers(): ManufacturersMap {
if (!this._manufacturers) {
throw new ZWaveError(
"The config has not been loaded yet!",
ZWaveErrorCodes.Driver_NotReady,
);
}
return this._manufacturers;
}
private _namedScales: NamedScalesGroupMap | undefined;
public get namedScales(): NamedScalesGroupMap {
if (!this._namedScales) {
throw new ZWaveError(
"The config has not been loaded yet!",
ZWaveErrorCodes.Driver_NotReady,
);
}
return this._namedScales;
}
private _sensorTypes: SensorTypeMap | undefined;
public get sensorTypes(): SensorTypeMap {
if (!this._sensorTypes) {
throw new ZWaveError(
"The config has not been loaded yet!",
ZWaveErrorCodes.Driver_NotReady,
);
}
return this._sensorTypes;
}
private _meters: MeterMap | undefined;
public get meters(): MeterMap {
if (!this._meters) {
throw new ZWaveError(
"The config has not been loaded yet!",
ZWaveErrorCodes.Driver_NotReady,
);
}
return this._meters;
}
private _basicDeviceClasses: BasicDeviceClassMap | undefined;
public get basicDeviceClasses(): BasicDeviceClassMap {
if (!this._basicDeviceClasses) {
throw new ZWaveError(
"The config has not been loaded yet!",
ZWaveErrorCodes.Driver_NotReady,
);
}
return this._basicDeviceClasses;
}
private _genericDeviceClasses: GenericDeviceClassMap | undefined;
public get genericDeviceClasses(): GenericDeviceClassMap {
if (!this._genericDeviceClasses) {
throw new ZWaveError(
"The config has not been loaded yet!",
ZWaveErrorCodes.Driver_NotReady,
);
}
return this._genericDeviceClasses;
}
private deviceConfigPriorityDir: string | undefined;
private index: DeviceConfigIndex | undefined;
private fulltextIndex: FulltextDeviceConfigIndex | undefined;
private _notifications: NotificationMap | undefined;
public get notifications(): NotificationMap {
if (!this._notifications) {
throw new ZWaveError(
"The config has not been loaded yet!",
ZWaveErrorCodes.Driver_NotReady,
);
}
return this._notifications;
}
private _useExternalConfig: boolean = false;
public get useExternalConfig(): boolean {
return this._useExternalConfig;
}
public async loadAll(): Promise<void> {
// If the environment option for an external config dir is set
// try to sync it and then use it
const syncResult = await syncExternalConfigDir(this.logger);
if (syncResult.success) {
this._useExternalConfig = true;
this.logger.print(
`Using external configuration dir ${externalConfigDir()}`,
);
this._configVersion = syncResult.version;
} else {
this._useExternalConfig = false;
this._configVersion = await getEmbeddedConfigVersion();
}
this.logger.print(`version ${this._configVersion}`, "info");
await this.loadDeviceClasses();
await this.loadManufacturers();
await this.loadDeviceIndex();
await this.loadNotifications();
await this.loadNamedScales();
await this.loadSensorTypes();
await this.loadMeters();
await this.loadIndicators();
}
public async loadManufacturers(): Promise<void> {
try {
this._manufacturers = await loadManufacturersInternal(
this._useExternalConfig,
);
} catch (e) {
// If the config file is missing or invalid, don't try to find it again
if (isZWaveError(e) && e.code === ZWaveErrorCodes.Config_Invalid) {
if (process.env.NODE_ENV !== "test") {
this.logger.print(
`Could not load manufacturers config: ${e.message}`,
"error",
);
}
if (!this._manufacturers) this._manufacturers = new Map();
} else {
// This is an unexpected error
throw e;
}
}
}
public async saveManufacturers(): Promise<void> {
if (!this._manufacturers) {
throw new ZWaveError(
"The config has not been loaded yet!",
ZWaveErrorCodes.Driver_NotReady,
);
}
await saveManufacturersInternal(this._manufacturers);
}
/**
* Looks up the name of the manufacturer with the given ID in the configuration DB
* @param manufacturerId The manufacturer id to look up
*/
public lookupManufacturer(manufacturerId: number): string | undefined {
if (!this._manufacturers) {
throw new ZWaveError(
"The config has not been loaded yet!",
ZWaveErrorCodes.Driver_NotReady,
);
}
return this._manufacturers.get(manufacturerId);
}
/**
* Add new manufacturers to configuration DB
* @param manufacturerId The manufacturer id to look up
* @param manufacturerName The manufacturer name
*/
public setManufacturer(
manufacturerId: number,
manufacturerName: string,
): void {
if (!this._manufacturers) {
throw new ZWaveError(
"The config has not been loaded yet!",
ZWaveErrorCodes.Driver_NotReady,
);
}
this._manufacturers.set(manufacturerId, manufacturerName);
}
public async loadIndicators(): Promise<void> {
try {
const config = await loadIndicatorsInternal(
this._useExternalConfig,
);
this._indicators = config.indicators;
this._indicatorProperties = config.properties;
} catch (e) {
// If the config file is missing or invalid, don't try to find it again
if (isZWaveError(e) && e.code === ZWaveErrorCodes.Config_Invalid) {
if (process.env.NODE_ENV !== "test") {
this.logger.print(
`Could not load indicators config: ${e.message}`,
"error",
);
}
if (!this._indicators) this._indicators = new Map();
if (!this._indicatorProperties)
this._indicatorProperties = new Map();
} else {
// This is an unexpected error
throw e;
}
}
}
/**
* Looks up the label for a given indicator id
*/
public lookupIndicator(indicatorId: number): string | undefined {
if (!this._indicators) {
throw new ZWaveError(
"The config has not been loaded yet!",
ZWaveErrorCodes.Driver_NotReady,
);
}
return this._indicators.get(indicatorId);
}
/**
* Looks up the property definition for a given indicator property id
*/
public lookupProperty(propertyId: number): IndicatorProperty | undefined {
if (!this._indicatorProperties) {
throw new ZWaveError(
"The config has not been loaded yet!",
ZWaveErrorCodes.Driver_NotReady,
);
}
return this._indicatorProperties.get(propertyId);
}
public async loadNamedScales(): Promise<void> {
try {
this._namedScales = await loadNamedScalesInternal(
this._useExternalConfig,
);
} catch (e) {
// If the config file is missing or invalid, don't try to find it again
if (isZWaveError(e) && e.code === ZWaveErrorCodes.Config_Invalid) {
if (process.env.NODE_ENV !== "test") {
this.logger.print(
`Could not load scales config: ${e.message}`,
"error",
);
}
if (!this._namedScales) this._namedScales = new Map();
} else {
// This is an unexpected error
throw e;
}
}
}
/**
* Looks up all scales defined under a given name
*/
public lookupNamedScaleGroup(name: string): ScaleGroup | undefined {
if (!this._namedScales) {
throw new ZWaveError(
"The config has not been loaded yet!",
ZWaveErrorCodes.Driver_NotReady,
);
}
return this._namedScales.get(name);
}
/** Looks up a scale definition for a given scale type */
public lookupNamedScale(name: string, scale: number): Scale {
const group = this.lookupNamedScaleGroup(name);
return group?.get(scale) ?? getDefaultScale(scale);
}
public async loadSensorTypes(): Promise<void> {
try {
this._sensorTypes = await loadSensorTypesInternal(
this,
this._useExternalConfig,
);
} catch (e) {
// If the config file is missing or invalid, don't try to find it again
if (isZWaveError(e) && e.code === ZWaveErrorCodes.Config_Invalid) {
if (process.env.NODE_ENV !== "test") {
this.logger.print(
`Could not load sensor types config: ${e.message}`,
"error",
);
}
if (!this._sensorTypes) this._sensorTypes = new Map();
} else {
// This is an unexpected error
throw e;
}
}
}
/**
* Looks up the configuration for a given sensor type
*/
public lookupSensorType(sensorType: number): SensorType | undefined {
if (!this._sensorTypes) {
throw new ZWaveError(
"The config has not been loaded yet!",
ZWaveErrorCodes.Driver_NotReady,
);
}
return this._sensorTypes.get(sensorType);
}
/** Looks up a scale definition for a given sensor type */
public lookupSensorScale(sensorType: number, scale: number): Scale {
const sensor = this.lookupSensorType(sensorType);
return sensor?.scales.get(scale) ?? getDefaultScale(scale);
}
public getSensorTypeName(sensorType: number): string {
const sensor = this.lookupSensorType(sensorType);
if (sensor) return sensor.label;
return `UNKNOWN (${num2hex(sensorType)})`;
}
public async loadMeters(): Promise<void> {
try {
this._meters = await loadMetersInternal(this._useExternalConfig);
} catch (e) {
// If the config file is missing or invalid, don't try to find it again
if (isZWaveError(e) && e.code === ZWaveErrorCodes.Config_Invalid) {
if (process.env.NODE_ENV !== "test") {
this.logger.print(
`Could not meters config: ${e.message}`,
"error",
);
}
if (!this._meters) this._meters = new Map();
} else {
// This is an unexpected error
throw e;
}
}
}
/**
* Looks up the notification configuration for a given notification type
*/
public lookupMeter(meterType: number): Meter | undefined {
if (!this._meters) {
throw new ZWaveError(
"The config has not been loaded yet!",
ZWaveErrorCodes.Driver_NotReady,
);
}
return this._meters.get(meterType);
}
public getMeterName(meterType: number): string {
const meter = this.lookupMeter(meterType);
return meter?.name ?? `UNKNOWN (${num2hex(meterType)})`;
}
/** Looks up a scale definition for a given meter type */
public lookupMeterScale(type: number, scale: number): MeterScale {
const meter = this.lookupMeter(type);
return meter?.scales.get(scale) ?? getDefaultMeterScale(scale);
}
public async loadDeviceClasses(): Promise<void> {
try {
const config = await loadDeviceClassesInternal(
this._useExternalConfig,
);
this._basicDeviceClasses = config.basicDeviceClasses;
this._genericDeviceClasses = config.genericDeviceClasses;
} catch (e) {
// If the config file is missing or invalid, don't try to find it again
if (isZWaveError(e) && e.code === ZWaveErrorCodes.Config_Invalid) {
if (process.env.NODE_ENV !== "test") {
this.logger.print(
`Could not load scales config: ${e.message}`,
"error",
);
}
if (!this._basicDeviceClasses)
this._basicDeviceClasses = new Map();
if (!this._genericDeviceClasses)
this._genericDeviceClasses = new Map();
} else {
// This is an unexpected error
throw e;
}
}
}
public lookupBasicDeviceClass(basic: number): BasicDeviceClass {
if (!this._basicDeviceClasses) {
throw new ZWaveError(
"The config has not been loaded yet!",
ZWaveErrorCodes.Driver_NotReady,
);
}
return {
key: basic,
label:
this._basicDeviceClasses.get(basic) ??
`UNKNOWN (${num2hex(basic)})`,
};
}
public lookupGenericDeviceClass(generic: number): GenericDeviceClass {
if (!this._genericDeviceClasses) {
throw new ZWaveError(
"The config has not been loaded yet!",
ZWaveErrorCodes.Driver_NotReady,
);
}
return (
this._genericDeviceClasses.get(generic) ??
getDefaultGenericDeviceClass(generic)
);
}
public lookupSpecificDeviceClass(
generic: number,
specific: number,
): SpecificDeviceClass {
const genericClass = this.lookupGenericDeviceClass(generic);
return (
genericClass.specific.get(specific) ??
getDefaultSpecificDeviceClass(genericClass, specific)
);
}
public async loadDeviceIndex(): Promise<void> {
try {
// The index of config files included in this package
const embeddedIndex = await loadDeviceIndexInternal(
this.logger,
this._useExternalConfig,
);
// A dynamic index of the user-defined priority device config files
const priorityIndex: DeviceConfigIndex = [];
if (this.deviceConfigPriorityDir) {
if (await pathExists(this.deviceConfigPriorityDir)) {
priorityIndex.push(
...(await generatePriorityDeviceIndex(
this.deviceConfigPriorityDir,
this.logger,
)),
);
} else {
this.logger.print(
`Priority device configuration directory ${this.deviceConfigPriorityDir} not found`,
"warn",
);
}
}
// Put the priority index in front, so the files get resolved first
this.index = [...priorityIndex, ...embeddedIndex];
} catch (e) {
// If the index file is missing or invalid, don't try to find it again
if (
(!isZWaveError(e) && e instanceof Error) ||
(isZWaveError(e) && e.code === ZWaveErrorCodes.Config_Invalid)
) {
// Fall back to no index on production systems
if (!this.index) this.index = [];
if (process.env.NODE_ENV !== "test") {
this.logger.print(
`Could not load or regenerate device config index: ${e.message}`,
"error",
);
// and don't throw
return;
}
// But fail hard in tests
throw e;
}
}
}
public getIndex(): DeviceConfigIndex | undefined {
return this.index;
}
public async loadFulltextDeviceIndex(): Promise<void> {
this.fulltextIndex = await loadFulltextDeviceIndexInternal(this.logger);
}
public getFulltextIndex(): FulltextDeviceConfigIndex | undefined {
return this.fulltextIndex;
}
/**
* Looks up the definition of a given device in the configuration DB, but does not evaluate conditional settings.
* @param manufacturerId The manufacturer id of the device
* @param productType The product type of the device
* @param productId The product id of the device
* @param firmwareVersion If known, configuration for a specific firmware version can be loaded.
* If this is `undefined` or not given, the first matching file with a defined firmware range will be returned.
*/
public async lookupDevicePreserveConditions(
manufacturerId: number,
productType: number,
productId: number,
firmwareVersion?: string,
): Promise<ConditionalDeviceConfig | undefined> {
// Load/regenerate the index if necessary
if (!this.index) await this.loadDeviceIndex();
// Look up the device in the index
const indexEntry = this.index!.find(
getDeviceEntryPredicate(
manufacturerId,
productType,
productId,
firmwareVersion,
),
);
if (indexEntry) {
const devicesDir = getDevicesPaths(
this._useExternalConfig ? externalConfigDir()! : configDir,
).devicesDir;
const filePath = path.isAbsolute(indexEntry.filename)
? indexEntry.filename
: path.join(devicesDir, indexEntry.filename);
if (!(await pathExists(filePath))) return;
// A config file is treated as am embedded one when it is located under the devices root dir
// or the external config dir
const isEmbedded = !path
.relative(devicesDir, filePath)
.startsWith("..");
try {
return await ConditionalDeviceConfig.from(
filePath,
isEmbedded,
{
// When looking for device files, fall back to the embedded config dir
rootDir: indexEntry.rootDir ?? devicesDir,
},
);
} catch (e) {
if (process.env.NODE_ENV !== "test") {
this.logger.print(
`Error loading device config ${filePath}: ${getErrorMessage(
e,
true,
)}`,
"error",
);
}
}
}
}
/**
* Looks up the definition of a given device in the configuration DB
* @param manufacturerId The manufacturer id of the device
* @param productType The product type of the device
* @param productId The product id of the device
* @param firmwareVersion If known, configuration for a specific firmware version can be loaded.
* If this is `undefined` or not given, the first matching file with a defined firmware range will be returned.
*/
public async lookupDevice(
manufacturerId: number,
productType: number,
productId: number,
firmwareVersion?: string,
): Promise<DeviceConfig | undefined> {
const ret = await this.lookupDevicePreserveConditions(
manufacturerId,
productType,
productId,
firmwareVersion,
);
return ret?.evaluate({
manufacturerId,
productType,
productId,
firmwareVersion,
});
}
public async loadNotifications(): Promise<void> {
try {
this._notifications = await loadNotificationsInternal(
this._useExternalConfig,
);
} catch (e) {
// If the config file is missing or invalid, don't try to find it again
if (isZWaveError(e) && e.code === ZWaveErrorCodes.Config_Invalid) {
if (process.env.NODE_ENV !== "test") {
this.logger.print(
`Could not load notifications config: ${e.message}`,
"error",
);
}
this._notifications = new Map();
} else {
// This is an unexpected error
throw e;
}
}
}
/**
* Looks up the notification configuration for a given notification type
*/
public lookupNotification(
notificationType: number,
): Notification | undefined {
if (!this._notifications) {
throw new ZWaveError(
"The config has not been loaded yet!",
ZWaveErrorCodes.Driver_NotReady,
);
}
return this._notifications.get(notificationType);
}
/**
* Looks up the notification configuration for a given notification type.
* If the config has not been loaded yet, this returns undefined.
*/
private lookupNotificationUnsafe(
notificationType: number,
): Notification | undefined {
return this._notifications?.get(notificationType);
}
public getNotificationName(notificationType: number): string {
return (
this.lookupNotificationUnsafe(notificationType)?.name ??
`Unknown (${num2hex(notificationType)})`
);
}
}
/** @internal */
export async function loadDeviceClassesInternal(
externalConfig?: boolean,
): Promise<{
basicDeviceClasses: BasicDeviceClassMap;
genericDeviceClasses: GenericDeviceClassMap;
}> {
const configPath = path.join(
(externalConfig && externalConfigDir()) || configDir,
"deviceClasses.json",
);
if (!(await pathExists(configPath))) {
throw new ZWaveError(
"The device classes config file does not exist!",
ZWaveErrorCodes.Config_Invalid,
);
}
try {
const fileContents = await readFile(configPath, "utf8");
const definition = JSON5.parse(fileContents);
if (!isObject(definition)) {
throwInvalidConfig(
"device classes",
`the dictionary is not an object`,
);
}
if (!isObject(definition.basic)) {
throwInvalidConfig(
"device classes",
`The "basic" property is not an object`,
);
}
if (!isObject(definition.generic)) {
throwInvalidConfig(
"device classes",
`The "generic" property is not an object`,
);
}
const basicDeviceClasses = new Map<number, string>();
for (const [key, basicClass] of Object.entries(definition.basic)) {
if (!hexKeyRegexNDigits.test(key)) {
throwInvalidConfig(
"device classes",
`found invalid key "${key}" in the basic device class definition. Device classes must have lowercase hexadecimal IDs.`,
);
}
if (typeof basicClass !== "string") {
throwInvalidConfig(
"device classes",
`basic device class "${key}" must be a string`,
);
}
const keyNum = parseInt(key.slice(2), 16);
basicDeviceClasses.set(keyNum, basicClass);
}
const genericDeviceClasses = new Map<number, GenericDeviceClass>();
for (const [key, genericDefinition] of Object.entries(
definition.generic,
)) {
if (!hexKeyRegexNDigits.test(key)) {
throwInvalidConfig(
"device classes",
`found invalid key "${key}" in the generic device class definition. Device classes must have lowercase hexadecimal IDs.`,
);
}
const keyNum = parseInt(key.slice(2), 16);
genericDeviceClasses.set(
keyNum,
new GenericDeviceClass(keyNum, genericDefinition as any),
);
}
return { basicDeviceClasses, genericDeviceClasses };
} catch (e) {
if (isZWaveError(e)) {
throw e;
} else {
throwInvalidConfig("device classes");
}
}
}
/** @internal */
export async function loadIndicatorsInternal(
externalConfig?: boolean,
): Promise<{
indicators: IndicatorMap;
properties: IndicatorPropertiesMap;
}> {
const indicatorsConfigPath = path.join(
(externalConfig && externalConfigDir()) || configDir,
"indicators.json",
);
if (!(await pathExists(indicatorsConfigPath))) {
throw new ZWaveError(
"The config file does not exist!",
ZWaveErrorCodes.Config_Invalid,
);
}
try {
const fileContents = await readFile(indicatorsConfigPath, "utf8");
const definition = JSON5.parse(fileContents);
if (!isObject(definition)) {
throwInvalidConfig("indicators", "the database is not an object");
}
if (!("indicators" in definition)) {
throwInvalidConfig(
"indicators",
`the required key "indicators" is missing`,
);
}
if (!("properties" in definition)) {
throwInvalidConfig(
"indicators",
`the required key "properties" is missing`,
);
}
const indicators = new Map<number, string>();
for (const [id, label] of Object.entries(definition.indicators)) {
if (!hexKeyRegexNDigits.test(id)) {
throwInvalidConfig(
"indicators",
`found invalid key "${id}" in "indicators". Indicators must have lowercase hexadecimal IDs.`,
);
}
if (typeof label !== "string") {
throwInvalidConfig(
"indicators",
`indicator "${id}" must be a string`,
);
}
const idNum = parseInt(id.slice(2), 16);
indicators.set(idNum, label);
}
const properties = new Map<number, IndicatorProperty>();
for (const [id, propDefinition] of Object.entries(
definition.properties,
)) {
if (!hexKeyRegexNDigits.test(id)) {
throwInvalidConfig(
"indicators",
`found invalid key "${id}" in "properties". Indicator properties must have lowercase hexadecimal IDs.`,
);
}
const idNum = parseInt(id.slice(2), 16);
properties.set(
idNum,
new IndicatorProperty(idNum, propDefinition as any),
);
}
return { indicators, properties };
} catch (e) {
if (isZWaveError(e)) {
throw e;
} else {
throwInvalidConfig("indicators");
}
}
}
/** @internal */
export async function loadMetersInternal(
externalConfig?: boolean,
): Promise<MeterMap> {
const configPath = path.join(
(externalConfig && externalConfigDir()) || configDir,
"meters.json",
);
if (!(await pathExists(configPath))) {
throw new ZWaveError(
"The config file does not exist!",
ZWaveErrorCodes.Config_Invalid,
);
}
try {
const fileContents = await readFile(configPath, "utf8");
const definition = JSON5.parse(fileContents);
if (!isObject(definition)) {
throwInvalidConfig("meters", "the database is not an object");
}
const meters = new Map();
for (const [id, meterDefinition] of Object.entries(definition)) {
if (!hexKeyRegexNDigits.test(id)) {
throwInvalidConfig(
"meters",
`found invalid key "${id}" at the root. Meters must have lowercase hexadecimal IDs.`,
);
}
const idNum = parseInt(id.slice(2), 16);
meters.set(idNum, new Meter(idNum, meterDefinition as JSONObject));
}
return meters;
} catch (e) {
if (isZWaveError(e)) {
throw e;
} else {
throwInvalidConfig("meters");
}
}
}
/** @internal */
export async function loadNotificationsInternal(
externalConfig?: boolean,
): Promise<NotificationMap> {
const configPath = path.join(
(externalConfig && externalConfigDir()) || configDir,
"notifications.json",
);
if (!(await pathExists(configPath))) {
throw new ZWaveError(
"The config file does not exist!",
ZWaveErrorCodes.Config_Invalid,
);
}
try {
const fileContents = await readFile(configPath, "utf8");
const definition = JSON5.parse(fileContents);
if (!isObject(definition)) {
throwInvalidConfig(
"notifications",
"the database is not an object",
);
}
const notifications = new Map();
for (const [id, ntfcnDefinition] of Object.entries(definition)) {
if (!hexKeyRegexNDigits.test(id)) {
throwInvalidConfig(
"notifications",
`found invalid key "${id}" at the root. Notifications must have lowercase hexadecimal IDs.`,
);
}
const idNum = parseInt(id.slice(2), 16);
notifications.set(
idNum,
new Notification(idNum, ntfcnDefinition as JSONObject),
);
}
return notifications;
} catch (e) {
if (isZWaveError(e)) {
throw e;
} else {
throwInvalidConfig("notifications");
}
}
}
/** @internal */
export async function loadNamedScalesInternal(
externalConfig?: boolean,
): Promise<NamedScalesGroupMap> {
const configPath = path.join(
(externalConfig && externalConfigDir()) || configDir,
"scales.json",
);
if (!(await pathExists(configPath))) {
throw new ZWaveError(
"The named scales config file does not exist!",
ZWaveErrorCodes.Config_Invalid,
);
}
try {
const fileContents = await readFile(configPath, "utf8");
const definition = JSON5.parse(fileContents);
if (!isObject(definition)) {
throwInvalidConfig(
"named scales",
`the dictionary is not an object`,
);
}
const namedScales = new Map<string, ScaleGroup>();
for (const [name, scales] of Object.entries(definition)) {
if (!/[\w\d]+/.test(name)) {
throwInvalidConfig(
"named scales",
`Name ${name} contains other characters than letters and numbers`,
);
}
const named: Map<number, Scale> & { name?: string } = new Map<
number,
Scale
>();
named.name = name;
for (const [key, scaleDefinition] of Object.entries(
scales as JSONObject,
)) {
if (!hexKeyRegexNDigits.test(key)) {
throwInvalidConfig(
"named scales",
`found invalid key "${key}" in the definition for "${name}". Scales must have lowercase hexadecimal IDs.`,
);
}
const keyNum = parseInt(key.slice(2), 16);
named.set(keyNum, new Scale(keyNum, scaleDefinition));
}
namedScales.set(name, named);
}
return namedScales;
} catch (e) {
if (isZWaveError(e)) {
throw e;
} else {
throwInvalidConfig("named scales");
}
}
}
/** @internal */
export async function loadSensorTypesInternal(
manager: ConfigManager,
externalConfig?: boolean,
): Promise<SensorTypeMap> {
const configPath = path.join(
(externalConfig && externalConfigDir()) || configDir,
"sensorTypes.json",
);
if (!(await pathExists(configPath))) {
throw new ZWaveError(
"The sensor types config file does not exist!",
ZWaveErrorCodes.Config_Invalid,
);
}
try {
const fileContents = await readFile(configPath, "utf8");
const definition = JSON5.parse(fileContents);
if (!isObject(definition)) {
throwInvalidConfig(
"sensor types",
`the dictionary is not an object`,
);
}
const sensorTypes = new Map();
for (const [key, sensorDefinition] of Object.entries(definition)) {
if (!hexKeyRegexNDigits.test(key)) {
throwInvalidConfig(
"sensor types",
`found invalid key "${key}" at the root. Sensor types must have lowercase hexadecimal IDs.`,
);
}
const keyNum = parseInt(key.slice(2), 16);
sensorTypes.set(
keyNum,
new SensorType(manager, keyNum, sensorDefinition as JSONObject),
);
}
return sensorTypes;
} catch (e) {
if (isZWaveError(e)) {
throw e;
} else {
throwInvalidConfig("sensor types");
}
}
}