Skip to content

Commit

Permalink
Controller energy (#128)
Browse files Browse the repository at this point in the history
* Add generic Polling logic

... this allows to define which attributes are polled and do this in an optimized way and init the polling interval and expose config

* Define Polling for custom energy states

* Adjust to new constructor

* Use Bridged device reachability over node connected status

* inspec matter,js errors for more infos

... because errors can be nested and are not included in default toString()

* Optimize device updates

* Improve infos and device config

* make linter happy

* adjust logging
  • Loading branch information
Apollon77 authored Nov 16, 2024
1 parent 9b369dc commit 87cd04a
Show file tree
Hide file tree
Showing 18 changed files with 516 additions and 135 deletions.
139 changes: 100 additions & 39 deletions src/lib/DeviceManagement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import {
type DeviceRefresh,
type DeviceStatus,
type InstanceDetails,
type JsonFormData,
} from '@iobroker/dm-utils';
import type { GeneralMatterNode, NodeDetails } from '../matter/GeneralMatterNode';
import type { GenericDeviceToIoBroker } from '../matter/to-iobroker/GenericDeviceToIoBroker';
import { GenericDeviceToIoBroker } from '../matter/to-iobroker/GenericDeviceToIoBroker';
import { getText, t } from './i18n';
import { decamelize } from './utils';

Expand Down Expand Up @@ -155,6 +156,12 @@ class MatterAdapterDeviceManagement extends DeviceManagement<MatterAdapter> {
description: t('Configure this node'),
handler: (id, context) => this.#handleConfigureNode(ioNode, context),
},
{
id: 'logNodeDebug',
icon: 'fa-solid fa-file-lines', // Why icon does not work??
description: t('Output Debug details this node'),
handler: (id, context) => this.#handleLogDebugNode(ioNode, context),
},
],
});

Expand Down Expand Up @@ -279,61 +286,114 @@ class MatterAdapterDeviceManagement extends DeviceManagement<MatterAdapter> {
return Promise.resolve({ refresh: false });
}

async #handleLogDebugNode(node: GeneralMatterNode, context: ActionContext): Promise<{ refresh: DeviceRefresh }> {
const debugInfos = 'TODO';

await context.showForm(
{
type: 'panel',
items: {
debugInfos: {
type: 'text',
label: t('Debug Infos'),
minRows: 30,
sm: 12,
disabled: true,
},
},
style: {
minWidth: 200,
},
},
{
data: { debugInfos },
title: t('Debug Infos'),
},
);

return { refresh: false };
}

async #handleConfigureNodeOrDevice(
title: string,
baseId: string,
context: ActionContext,
nodeOrDevice: GeneralMatterNode | GenericDeviceToIoBroker,
): Promise<{ refresh: DeviceRefresh }> {
const obj = await this.adapter.getObjectAsync(baseId);

//const node = nodeOrDevice instanceof GeneralMatterNode ? nodeOrDevice : undefined;
const device = nodeOrDevice instanceof GenericDeviceToIoBroker ? nodeOrDevice : undefined;

const items: Record<string, ConfigItemAny> = {
exposeMatterApplicationClusterData: {
type: 'select',
label: t('Expose Matter Application Cluster Data'),
options: [
{ label: 'Yes', value: 'true' },
{ label: 'No', value: 'false' },
{ label: 'Default', value: '' },
],
sm: 12,
},
exposeMatterSystemClusterData: {
type: 'select',
label: t('Expose Matter System Cluster Data'),
options: [
{ label: 'Yes', value: 'true' },
{ label: 'No', value: 'false' },
{ label: 'Default', value: '' },
],
sm: 12,
},
};
const data: JsonFormData = {
exposeMatterApplicationClusterData:
typeof obj?.native.exposeMatterApplicationClusterData !== 'boolean'
? ''
: obj?.native.exposeMatterApplicationClusterData
? 'true'
: 'false',
exposeMatterSystemClusterData:
typeof obj?.native.exposeMatterSystemClusterData !== 'boolean'
? ''
: obj?.native.exposeMatterSystemClusterData
? 'true'
: 'false',
};

if (device !== undefined) {
const deviceConfig = device.deviceConfiguration;
if (deviceConfig.pollInterval !== undefined) {
items.pollInterval = {
type: 'number',
label: t('Energy Attribute Polling Interval (s)'),
min: 30,
max: 2147482,
sm: 12,
};
data.pollInterval = deviceConfig.pollInterval;
}
}

const result = await context.showForm(
{
type: 'panel',
items: {
exposeMatterApplicationClusterData: {
type: 'select',
label: t('Expose Matter Application Cluster Data'),
options: [
{ label: 'Yes', value: 'true' },
{ label: 'No', value: 'false' },
{ label: 'Default', value: '' },
],
sm: 12,
},
exposeMatterSystemClusterData: {
type: 'select',
label: t('Expose Matter System Cluster Data'),
options: [
{ label: 'Yes', value: 'true' },
{ label: 'No', value: 'false' },
{ label: 'Default', value: '' },
],
sm: 12,
},
},
items,
style: {
minWidth: 200,
},
},
{
data: {
exposeMatterApplicationClusterData:
typeof obj?.native.exposeMatterApplicationClusterData !== 'boolean'
? ''
: obj?.native.exposeMatterApplicationClusterData
? 'true'
: 'false',
exposeMatterSystemClusterData:
typeof obj?.native.exposeMatterSystemClusterData !== 'boolean'
? ''
: obj?.native.exposeMatterSystemClusterData
? 'true'
: 'false',
},
data,
title: t(title),
},
);

if (result?.pollInterval !== undefined && device !== undefined) {
device.setDeviceConfiguration({ pollInterval: result.pollInterval });
}

if (
result?.exposeMatterApplicationClusterData !== undefined &&
result?.exposeMatterSystemClusterData !== undefined
Expand All @@ -342,6 +402,7 @@ class MatterAdapterDeviceManagement extends DeviceManagement<MatterAdapter> {
native: {
exposeMatterApplicationClusterData: strToBool(result.exposeMatterApplicationClusterData),
exposeMatterSystemClusterData: strToBool(result.exposeMatterSystemClusterData),
...(device !== undefined ? device.deviceConfiguration : {}),
},
});
}
Expand All @@ -351,7 +412,7 @@ class MatterAdapterDeviceManagement extends DeviceManagement<MatterAdapter> {
async #handleConfigureNode(node: GeneralMatterNode, context: ActionContext): Promise<{ refresh: DeviceRefresh }> {
this.adapter.log.info(`Configure node ${node.nodeId}`);

return await this.#handleConfigureNodeOrDevice('Configure node', node.nodeBaseId, context);
return await this.#handleConfigureNodeOrDevice('Configure node', node.nodeBaseId, context, node);
}

async #handleConfigureDevice(
Expand All @@ -360,7 +421,7 @@ class MatterAdapterDeviceManagement extends DeviceManagement<MatterAdapter> {
): Promise<{ refresh: DeviceRefresh }> {
this.adapter.log.info(`Configure device ${device.name}`);

return await this.#handleConfigureNodeOrDevice('Configure device', device.baseId, context);
return await this.#handleConfigureNodeOrDevice('Configure device', device.baseId, context, device);
}

async #handleIdentifyDevice(
Expand Down
2 changes: 1 addition & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ export class MatterAdapter extends utils.Adapter {
// controller sub node changed
const nodeId = objParts[1];
await this.syncControllerNode(nodeId, obj as ioBroker.FolderObject);
} else if (objParts[0] === 'controller' && objPartsLength === 3 && obj?.type === 'device') {
} else if (objParts[0] === 'controller' && objPartsLength > 2 && obj?.type === 'device') {
// controller node device sub node changed
const nodeId = objParts[1];
const nodeObj = await this.getObjectAsync(`controller.${nodeId}`);
Expand Down
63 changes: 33 additions & 30 deletions src/matter/ControllerNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type { MatterControllerConfig } from '../../src-admin/src/types';
import type { MatterAdapter } from '../main';
import { GeneralMatterNode, type PairedNodeConfig } from './GeneralMatterNode';
import type { GeneralNode, MessageResponse } from './GeneralNode';
import { inspect } from 'util';

export interface ControllerCreateOptions {
adapter: MatterAdapter;
Expand Down Expand Up @@ -184,8 +185,9 @@ class Controller implements GeneralNode {
return await this.completeCommissioningForNode(message.peerNodeId, message.discoveryData);
}
} catch (error) {
this.#adapter.log.warn(`Error while executing command "${command}": ${error.stack}`);
return { error: `Error while executing command "${command}": ${error.message}` };
const errorText = inspect(error, { depth: 10 });
this.#adapter.log.warn(`Error while executing command "${command}": ${errorText}`);
return { error: `Error while executing command "${command}": ${error.message}`, result: false };
}

return { error: `Unknown command "${command}"` };
Expand Down Expand Up @@ -265,7 +267,13 @@ class Controller implements GeneralNode {

await this.#adapter.setState('controller.info.discovering', false, true);

await this.#commissioningController.start();
try {
await this.#commissioningController.start();
} catch (error) {
const errorText = inspect(error, { depth: 10 });
this.#adapter.log.error(`Failed to start the controller: ${errorText}`);
return;
}

// get nodes
const nodes = this.#commissioningController.getCommissionedNodes();
Expand Down Expand Up @@ -369,37 +377,32 @@ class Controller implements GeneralNode {
}

// this.#adapter.log.debug(`Commissioning ... ${JSON.stringify(options)}`);
try {
if (passcode === undefined) {
throw new Error('Passcode is missing');
}
if (passcode === undefined) {
throw new Error('Passcode is missing');
}

const options: NodeCommissioningOptions = {
...this.nodeConnectSettings,
commissioning: commissioningOptions,
discovery: {
commissionableDevice: device || undefined,
identifierData:
longDiscriminator !== undefined
? { longDiscriminator }
: shortDiscriminator !== undefined
? { shortDiscriminator }
: vendorId !== undefined
? { vendorId, productId }
: undefined,
},
passcode,
};
const options: NodeCommissioningOptions = {
...this.nodeConnectSettings,
commissioning: commissioningOptions,
discovery: {
commissionableDevice: device || undefined,
identifierData:
longDiscriminator !== undefined
? { longDiscriminator }
: shortDiscriminator !== undefined
? { shortDiscriminator }
: vendorId !== undefined
? { vendorId, productId }
: undefined,
},
passcode,
};

const nodeId = await this.#commissioningController.commissionNode(options);
const nodeId = await this.#commissioningController.commissionNode(options);

await this.registerCommissionedNode(nodeId);
await this.registerCommissionedNode(nodeId);

return { result: true, nodeId: nodeId.toString() };
} catch (e) {
this.#adapter.log.info(`Commissioning failed: ${e.stack}`);
return { error: e.message, result: false };
}
return { result: true, nodeId: nodeId.toString() };
}

async completeCommissioningForNode(nodeId: NodeId, discoveryData?: DiscoveryData): Promise<AddDeviceResult> {
Expand Down
63 changes: 40 additions & 23 deletions src/matter/DeviceNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,26 +85,32 @@ class Device extends BaseServerNode {
// The device type to announce we use from the first returned endpoint of the device
const deviceType = endpoints[0].type.deviceType;

this.serverNode = await ServerNode.create({
id: this.#parameters.uuid,
network: {
port: this.#parameters.port,
},
productDescription: {
name: deviceName,
deviceType,
},
basicInformation: {
vendorName,
vendorId: VendorId(vendorId),
nodeLabel: productName,
productName,
productLabel: productName,
productId,
serialNumber: uniqueId,
uniqueId: md5(uniqueId),
},
});
try {
this.serverNode = await ServerNode.create({
id: this.#parameters.uuid,
network: {
port: this.#parameters.port,
},
productDescription: {
name: deviceName,
deviceType,
},
basicInformation: {
vendorName,
vendorId: VendorId(vendorId),
nodeLabel: productName,
productName,
productLabel: productName,
productId,
serialNumber: uniqueId,
uniqueId: md5(uniqueId),
},
});
} catch (error) {
const errorText = inspect(error, { depth: 10 });
this.adapter.log.error(`Error creating device ${this.#parameters.uuid}: ${errorText}`);
return;
}

if (this.#deviceOptions?.noComposed) {
// No composed means we remove all beside first returned endpoint
Expand Down Expand Up @@ -158,13 +164,24 @@ class Device extends BaseServerNode {
if (!this.serverNode) {
return;
}
await this.serverNode.start();
this.#started = true;
try {
await this.serverNode.start();
this.#started = true;
} catch (error) {
const errorText = inspect(error, { depth: 10 });
this.adapter.log.error(`Error starting device ${this.#parameters.uuid}: ${errorText}`);
return;
}
await this.updateUiState();
}

async stop(): Promise<void> {
await this.serverNode?.close();
try {
await this.serverNode?.close();
} catch (error) {
const errorText = inspect(error, { depth: 10 });
this.adapter.log.error(`Error stopping device ${this.#parameters.uuid}: ${errorText}`);
}
await this.#device.destroy();
this.serverNode = undefined;
this.#started = false;
Expand Down
Loading

0 comments on commit 87cd04a

Please sign in to comment.