Skip to content

Commit

Permalink
updates
Browse files Browse the repository at this point in the history
  • Loading branch information
Koenkk committed Aug 28, 2024
1 parent 2cc5138 commit 76e274b
Show file tree
Hide file tree
Showing 6 changed files with 55 additions and 37 deletions.
2 changes: 1 addition & 1 deletion lib/extension/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,8 +394,8 @@ export default class Bridge extends Extension {
throw new Error(`'${value}' is not an allowed value, allowed: ${allowed}`);
}

await this.enableDisableExtension(value, 'HomeAssistant');
settings.set(['homeassistant'], value);
await this.enableDisableExtension(value, 'HomeAssistant');
await this.publishInfo();
return utils.getResponse(message, {value});
}
Expand Down
23 changes: 14 additions & 9 deletions lib/extension/groups.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import assert from 'assert';
import bind from 'bind-decorator';
import equals from 'fast-deep-equal/es6';
import stringify from 'json-stable-stringify-without-jsonify';
Expand All @@ -7,7 +8,7 @@ import Device from '../model/device';
import Group from '../model/group';
import logger from '../util/logger';
import * as settings from '../util/settings';
import utils from '../util/utils';
import utils, {isLightExpose} from '../util/utils';
import Extension from './extension';

const TOPIC_REGEX = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/group/members/(remove|add|remove_all)$`);
Expand All @@ -16,13 +17,13 @@ const LEGACY_TOPIC_REGEX_REMOVE_ALL = new RegExp(`^${settings.get().mqtt.base_to

const STATE_PROPERTIES: Readonly<Record<string, (value: string, exposes: zhc.Expose[]) => boolean>> = {
state: () => true,
brightness: (value, exposes) => exposes.some((e) => e.type === 'light' && e.features.some((f) => f.name === 'brightness')),
color_temp: (value, exposes) => exposes.some((e) => e.type === 'light' && e.features.some((f) => f.name === 'color_temp')),
color: (value, exposes) => exposes.some((e) => e.type === 'light' && e.features.some((f) => f.name === 'color_xy' || f.name === 'color_hs')),
brightness: (value, exposes) => exposes.some((e) => isLightExpose(e) && e.features.some((f) => f.name === 'brightness')),

Check failure on line 20 in lib/extension/groups.ts

View workflow job for this annotation

GitHub Actions / tests (macos-latest, 18)

'e.features' is possibly 'undefined'.

Check failure on line 20 in lib/extension/groups.ts

View workflow job for this annotation

GitHub Actions / tests (macos-latest, 20)

'e.features' is possibly 'undefined'.

Check failure on line 20 in lib/extension/groups.ts

View workflow job for this annotation

GitHub Actions / ci

'e.features' is possibly 'undefined'.

Check failure on line 20 in lib/extension/groups.ts

View workflow job for this annotation

GitHub Actions / tests (ubuntu-latest, 22)

'e.features' is possibly 'undefined'.

Check failure on line 20 in lib/extension/groups.ts

View workflow job for this annotation

GitHub Actions / tests (macos-latest, 22)

'e.features' is possibly 'undefined'.

Check failure on line 20 in lib/extension/groups.ts

View workflow job for this annotation

GitHub Actions / tests (ubuntu-latest, 18)

'e.features' is possibly 'undefined'.

Check failure on line 20 in lib/extension/groups.ts

View workflow job for this annotation

GitHub Actions / tests (ubuntu-latest, 20)

'e.features' is possibly 'undefined'.

Check failure on line 20 in lib/extension/groups.ts

View workflow job for this annotation

GitHub Actions / tests (windows-latest, 22)

'e.features' is possibly 'undefined'.

Check failure on line 20 in lib/extension/groups.ts

View workflow job for this annotation

GitHub Actions / tests (windows-latest, 20)

'e.features' is possibly 'undefined'.

Check failure on line 20 in lib/extension/groups.ts

View workflow job for this annotation

GitHub Actions / tests (windows-latest, 18)

'e.features' is possibly 'undefined'.
color_temp: (value, exposes) => exposes.some((e) => isLightExpose(e) && e.features.some((f) => f.name === 'color_temp')),

Check failure on line 21 in lib/extension/groups.ts

View workflow job for this annotation

GitHub Actions / tests (macos-latest, 18)

'e.features' is possibly 'undefined'.

Check failure on line 21 in lib/extension/groups.ts

View workflow job for this annotation

GitHub Actions / tests (macos-latest, 20)

'e.features' is possibly 'undefined'.

Check failure on line 21 in lib/extension/groups.ts

View workflow job for this annotation

GitHub Actions / ci

'e.features' is possibly 'undefined'.

Check failure on line 21 in lib/extension/groups.ts

View workflow job for this annotation

GitHub Actions / tests (ubuntu-latest, 22)

'e.features' is possibly 'undefined'.

Check failure on line 21 in lib/extension/groups.ts

View workflow job for this annotation

GitHub Actions / tests (macos-latest, 22)

'e.features' is possibly 'undefined'.

Check failure on line 21 in lib/extension/groups.ts

View workflow job for this annotation

GitHub Actions / tests (ubuntu-latest, 18)

'e.features' is possibly 'undefined'.

Check failure on line 21 in lib/extension/groups.ts

View workflow job for this annotation

GitHub Actions / tests (ubuntu-latest, 20)

'e.features' is possibly 'undefined'.

Check failure on line 21 in lib/extension/groups.ts

View workflow job for this annotation

GitHub Actions / tests (windows-latest, 22)

'e.features' is possibly 'undefined'.

Check failure on line 21 in lib/extension/groups.ts

View workflow job for this annotation

GitHub Actions / tests (windows-latest, 20)

'e.features' is possibly 'undefined'.

Check failure on line 21 in lib/extension/groups.ts

View workflow job for this annotation

GitHub Actions / tests (windows-latest, 18)

'e.features' is possibly 'undefined'.
color: (value, exposes) => exposes.some((e) => isLightExpose(e) && e.features.some((f) => f.name === 'color_xy' || f.name === 'color_hs')),

Check failure on line 22 in lib/extension/groups.ts

View workflow job for this annotation

GitHub Actions / tests (macos-latest, 18)

'e.features' is possibly 'undefined'.

Check failure on line 22 in lib/extension/groups.ts

View workflow job for this annotation

GitHub Actions / tests (macos-latest, 20)

'e.features' is possibly 'undefined'.

Check failure on line 22 in lib/extension/groups.ts

View workflow job for this annotation

GitHub Actions / ci

'e.features' is possibly 'undefined'.

Check failure on line 22 in lib/extension/groups.ts

View workflow job for this annotation

GitHub Actions / tests (ubuntu-latest, 22)

'e.features' is possibly 'undefined'.

Check failure on line 22 in lib/extension/groups.ts

View workflow job for this annotation

GitHub Actions / tests (macos-latest, 22)

'e.features' is possibly 'undefined'.

Check failure on line 22 in lib/extension/groups.ts

View workflow job for this annotation

GitHub Actions / tests (ubuntu-latest, 18)

'e.features' is possibly 'undefined'.

Check failure on line 22 in lib/extension/groups.ts

View workflow job for this annotation

GitHub Actions / tests (ubuntu-latest, 20)

'e.features' is possibly 'undefined'.

Check failure on line 22 in lib/extension/groups.ts

View workflow job for this annotation

GitHub Actions / tests (windows-latest, 22)

'e.features' is possibly 'undefined'.

Check failure on line 22 in lib/extension/groups.ts

View workflow job for this annotation

GitHub Actions / tests (windows-latest, 20)

'e.features' is possibly 'undefined'.

Check failure on line 22 in lib/extension/groups.ts

View workflow job for this annotation

GitHub Actions / tests (windows-latest, 18)

'e.features' is possibly 'undefined'.
color_mode: (value, exposes) =>
exposes.some(
(e) =>
e.type === 'light' &&
isLightExpose(e) &&
(e.features.some((f) => f.name === `color_${value}`) || (value === 'color_temp' && e.features.some((f) => f.name === 'color_temp'))),
),
};
Expand Down Expand Up @@ -105,7 +106,7 @@ export default class Groups extends Extension {
// In zigbee but not in settings
for (const endpoint of zigbeeGroup.zh.members) {
if (!settingsEndpoints.includes(endpoint)) {
const deviceName = settings.getDevice(endpoint.getDevice().ieeeAddr).friendly_name;
const deviceName = settings.getDevice(endpoint.getDevice().ieeeAddr)!.friendly_name;

await addRemoveFromGroup('remove', deviceName, settingGroup.friendly_name, endpoint, zigbeeGroup);
}
Expand All @@ -114,7 +115,7 @@ export default class Groups extends Extension {

for (const zigbeeGroup of this.zigbee.groupsIterator((zg) => !settingsGroups.some((sg) => sg.ID === zg.groupID))) {
for (const endpoint of zigbeeGroup.zh.members) {
const deviceName = settings.getDevice(endpoint.getDevice().ieeeAddr).friendly_name;
const deviceName = settings.getDevice(endpoint.getDevice().ieeeAddr)!.friendly_name;

await addRemoveFromGroup('remove', deviceName, zigbeeGroup.ID, endpoint, zigbeeGroup);
}
Expand Down Expand Up @@ -229,7 +230,7 @@ export default class Groups extends Extension {

private areAllMembersOff(group: Group): boolean {
for (const member of group.zh.members) {
const device = this.zigbee.resolveEntity(member.getDevice());
const device = this.zigbee.resolveEntity(member.getDevice())!;

if (this.state.exists(device)) {
const state = this.state.get(device);
Expand Down Expand Up @@ -290,7 +291,7 @@ export default class Groups extends Extension {

/* istanbul ignore else */
if (settings.get().advanced.legacy_api) {
const message = {friendly_name: data.message, group: legacyTopicRegexMatch[1], error: "entity doesn't exists"};
const message = {friendly_name: data.message, group: legacyTopicRegexMatch![1], error: "entity doesn't exists"};

await this.mqtt.publish('bridge/log', stringify({type: `device_group_${type}_failed`, message}));
}
Expand Down Expand Up @@ -371,6 +372,7 @@ export default class Groups extends Extension {
const changedGroups: Group[] = [];

if (!error) {
assert(resolvedEntityEndpoint, '`resolvedEntityEndpoint` is missing');
try {
const keys = [
`${resolvedEntityDevice.ieeeAddr}/${resolvedEntityEndpoint.ID}`,
Expand All @@ -389,6 +391,7 @@ export default class Groups extends Extension {
}

if (type === 'add') {
assert(resolvedEntityGroup, '`resolvedEntityGroup` is missing');
logger.info(`Adding '${resolvedEntityDevice.name}' to '${resolvedEntityGroup.name}'`);
await resolvedEntityEndpoint.addToGroup(resolvedEntityGroup.zh);
settings.addDeviceToGroup(resolvedEntityGroup.ID.toString(), keys);
Expand All @@ -401,6 +404,7 @@ export default class Groups extends Extension {
await this.mqtt.publish('bridge/log', stringify({type: `device_group_add`, message}));
}
} else if (type === 'remove') {
assert(resolvedEntityGroup, '`resolvedEntityGroup` is missing');
logger.info(`Removing '${resolvedEntityDevice.name}' from '${resolvedEntityGroup.name}'`);
await resolvedEntityEndpoint.removeFromGroup(resolvedEntityGroup.zh);
settings.removeDeviceFromGroup(resolvedEntityGroup.ID.toString(), keys);
Expand Down Expand Up @@ -453,6 +457,7 @@ export default class Groups extends Extension {
if (error) {
logger.error(error);
} else {
assert(resolvedEntityEndpoint, '`resolvedEntityEndpoint` is missing');
for (const group of changedGroups) {
this.eventBus.emitGroupMembersChanged({group, action: type, endpoint: resolvedEntityEndpoint, skipDisableReporting});
}
Expand Down
16 changes: 11 additions & 5 deletions lib/extension/homeassistant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,11 @@ class Bridge {
*/
export default class HomeAssistant extends Extension {
private discovered: {[s: string]: Discovered} = {};
private discoveryTopic = settings.get().homeassistant.discovery_topic;
private discoveryRegex = new RegExp(`${settings.get().homeassistant.discovery_topic}/(.*)/(.*)/(.*)/config`);
private discoveryTopic: string;
private discoveryRegex: RegExp;
private discoveryRegexWoTopic = new RegExp(`(.*)/(.*)/(.*)/config`);
private statusTopic = settings.get().homeassistant.status_topic;
private entityAttributes = settings.get().homeassistant.legacy_entity_attributes;
private statusTopic: string;
private entityAttributes: boolean;
// @ts-expect-error initialized in `start`
private zigbee2MQTTVersion: string;
// @ts-expect-error initialized in `start`
Expand All @@ -191,7 +191,13 @@ export default class HomeAssistant extends Extension {
throw new Error('Home Assistant integration is not possible with attribute output!');
}

if (settings.get().homeassistant.discovery_topic === settings.get().mqtt.base_topic) {
const haSettings = settings.get().homeassistant;
assert(haSettings, 'Home Assistant extension used without settings');
this.discoveryTopic = haSettings.discovery_topic;
this.discoveryRegex = new RegExp(`${haSettings.discovery_topic}/(.*)/(.*)/(.*)/config`);
this.statusTopic = haSettings.status_topic;
this.entityAttributes = haSettings.legacy_entity_attributes;
if (haSettings.discovery_topic === settings.get().mqtt.base_topic) {
throw new Error(`'homeassistant.discovery_topic' cannot not be equal to the 'mqtt.base_topic' (got '${settings.get().mqtt.base_topic}')`);
}
}
Expand Down
6 changes: 4 additions & 2 deletions lib/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import type * as zhc from 'zigbee-herdsman-converters';

import {LogLevel} from 'lib/util/settings';

type OptionalProps<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

declare global {
// Define some class types as global
type EventBus = TypeEventBus;
Expand Down Expand Up @@ -184,8 +186,8 @@ declare global {
ssl_cert?: string;
ssl_key?: string;
};
devices?: {[s: string]: DeviceOptions};
groups?: {[s: string]: Omit<GroupOptions, 'ID'>};
devices: {[s: string]: DeviceOptions};
groups: {[s: string]: OptionalProps<Omit<GroupOptions, 'ID'>, 'devices'>};
device_options: KeyValue;
advanced: {
legacy_api: boolean;
Expand Down
40 changes: 20 additions & 20 deletions lib/util/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,25 @@ import data from './data';
import schemaJson from './settings.schema.json';
import utils from './utils';
import yaml, {YAMLFileException} from './yaml';
export let schema = schemaJson;
export let schema: KeyValue = schemaJson;

// @ts-expect-error
schema = {};
objectAssignDeep(schema, schemaJson);

// Remove legacy settings from schema
{
// @ts-expect-error
delete schema.properties.advanced.properties.homeassistant_discovery_topic;
// @ts-expect-error
delete schema.properties.advanced.properties.homeassistant_legacy_entity_attributes;
// @ts-expect-error
delete schema.properties.advanced.properties.homeassistant_legacy_triggers;
// @ts-expect-error
delete schema.properties.advanced.properties.homeassistant_status_topic;
// @ts-expect-error
delete schema.properties.advanced.properties.soft_reset_timeout;
// @ts-expect-error
delete schema.properties.advanced.properties.report;
// @ts-expect-error
delete schema.properties.advanced.properties.baudrate;
// @ts-expect-error
delete schema.properties.advanced.properties.rtscts;
// @ts-expect-error
delete schema.properties.advanced.properties.ikea_ota_use_test_url;
// @ts-expect-error
delete schema.properties.experimental;
// @ts-expect-error
delete schemaJson.properties.whitelist;
// @ts-expect-error
delete schemaJson.properties.ban;
delete (schemaJson as KeyValue).properties.whitelist;
delete (schemaJson as KeyValue).properties.ban;
}

/** NOTE: by order of priority, lower index is lower level (more important) */
Expand Down Expand Up @@ -133,6 +120,9 @@ let _settings: Partial<Settings> | undefined;
let _settingsWithDefaults: Settings | undefined;

function loadSettingsWithDefaults(): void {
if (!_settings) {
_settings = read();
}
_settingsWithDefaults = objectAssignDeep({}, defaults, getInternalSettings()) as Settings;

if (!_settingsWithDefaults.devices) {
Expand Down Expand Up @@ -164,6 +154,7 @@ function loadSettingsWithDefaults(): void {
const s = typeof _settingsWithDefaults.homeassistant === 'object' ? _settingsWithDefaults.homeassistant : {};
// @ts-expect-error
_settingsWithDefaults.homeassistant = {};
// @ts-expect-error
objectAssignDeep(_settingsWithDefaults.homeassistant, defaults, sLegacy, s);
}

Expand All @@ -172,13 +163,16 @@ function loadSettingsWithDefaults(): void {
const s = typeof _settingsWithDefaults.availability === 'object' ? _settingsWithDefaults.availability : {};
// @ts-expect-error
_settingsWithDefaults.availability = {};
// @ts-expect-error
objectAssignDeep(_settingsWithDefaults.availability, defaults, s);
}

if (_settingsWithDefaults.frontend) {
const defaults = {port: 8080, auth_token: false};
const s = typeof _settingsWithDefaults.frontend === 'object' ? _settingsWithDefaults.frontend : {};
// @ts-expect-error
_settingsWithDefaults.frontend = {};
// @ts-expect-error
objectAssignDeep(_settingsWithDefaults.frontend, defaults, s);
}

Expand Down Expand Up @@ -290,7 +284,6 @@ function write(): void {

yaml.writeIfChanged(file, toWrite);

_settings = read();
loadSettingsWithDefaults();
}

Expand Down Expand Up @@ -444,23 +437,30 @@ function applyEnvironmentVariables(settings: Partial<Settings>): void {

if (envVariable) {
const setting = path.reduce((acc, val) => {
// @ts-expect-error
acc[val] = acc[val] || {};
// @ts-expect-error
return acc[val];
}, settings);

if (type.indexOf('object') >= 0 || type.indexOf('array') >= 0) {
try {
// @ts-expect-error
setting[key] = JSON.parse(envVariable);
} catch {
// @ts-expect-error
setting[key] = envVariable;
}
} else if (type.indexOf('number') >= 0) {
// @ts-expect-error
setting[key] = (envVariable as unknown as number) * 1;
} else if (type.indexOf('boolean') >= 0) {
// @ts-expect-error
setting[key] = envVariable.toLowerCase() === 'true';
} else {
/* istanbul ignore else */
if (type.indexOf('string') >= 0) {
// @ts-expect-error
setting[key] = envVariable;
}
}
Expand Down Expand Up @@ -496,7 +496,7 @@ export function get(): Settings {
loadSettingsWithDefaults();
}

return _settingsWithDefaults;
return _settingsWithDefaults!;
}

export function set(path: string[], value: string | number | boolean | KeyValue): void {
Expand Down Expand Up @@ -646,14 +646,14 @@ export function blockDevice(ID: string): void {
export function removeDevice(IDorName: string): void {
const device = getDeviceThrowIfNotExists(IDorName);
const settings = getInternalSettings();
delete settings.devices[device.ID];
delete settings.devices?.[device.ID];

// Remove device from groups
if (settings.groups) {
const regex = new RegExp(`^(${device.friendly_name}|${device.ID})(/[^/]+)?$`);

for (const group of Object.values(settings.groups).filter((g) => g.devices)) {
group.devices = group.devices.filter((device) => !device.match(regex));
group.devices = group.devices?.filter((device) => !device.match(regex));
}
}

Expand Down
5 changes: 5 additions & 0 deletions lib/util/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type * as zhc from 'zigbee-herdsman-converters';

import assert from 'assert';
import equals from 'fast-deep-equal/es6';
import fs from 'fs';
import humanizeDuration from 'humanize-duration';
Expand Down Expand Up @@ -393,6 +394,10 @@ export function isLightExpose(expose: zhc.Expose): expose is zhc.Light {
return expose.type === 'light';
}

export function assertLightExpose(expose: zhc.Expose): asserts expose is zhc.Light {
assert(expose.type === 'light');
}

function getScenes(entity: zh.Endpoint | zh.Group): Scene[] {
const scenes: {[id: number]: Scene} = {};
const endpoints = isZHEndpoint(entity) ? [entity] : entity.members;
Expand Down

0 comments on commit 76e274b

Please sign in to comment.