From 8dd278c67b5f669e5fa8c227f01537b1d7b9ddd1 Mon Sep 17 00:00:00 2001 From: Luligu Date: Mon, 2 Dec 2024 14:23:23 +0100 Subject: [PATCH] Release 1.2.0 --- .gitignore | 6 +- .npmignore | 3 +- CHANGELOG.md | 2 +- package.json | 3 +- src/index.test.ts | 13 +-- src/platform.test.ts | 182 ++++++++++++++++++++++++++++++++++++--- src/platform.ts | 22 +++-- tsconfig.production.json | 12 +-- 8 files changed, 199 insertions(+), 44 deletions(-) diff --git a/.gitignore b/.gitignore index af69613..f7bf4c4 100644 --- a/.gitignore +++ b/.gitignore @@ -42,7 +42,5 @@ coverage/ hs_err_pid* replay_pid* -# zigbee2mqtt -bridge-info.json -bridge-devices.json -bridge-groups.json \ No newline at end of file +temp +src/mock \ No newline at end of file diff --git a/.npmignore b/.npmignore index 64fa4db..0fd8f85 100644 --- a/.npmignore +++ b/.npmignore @@ -165,4 +165,5 @@ screenshot TODO.md yellow-button.png create-release.js -tsconfig.* \ No newline at end of file +tsconfig.* +temp diff --git a/CHANGELOG.md b/CHANGELOG.md index 6730fc9..24e5135 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ If you like this project and find it useful, please consider giving it a star on All notable changes to this project will be documented in this file. -## [1.2.0] - 2024-11-30 +## [1.2.0] - 2024-12-02 ### Added diff --git a/package.json b/package.json index 7dcff4a..7f4ee5e 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "test:verbose": "node --experimental-vm-modules node_modules/jest/bin/jest.js --verbose", "test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch", "test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage", + "test:platform": "node --experimental-vm-modules node_modules/jest/bin/jest.js src/platform.test.ts --verbose --coverage", "lint": "eslint --max-warnings=0", "lint:fix": "eslint --max-warnings=0 --fix", "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,md}\"", @@ -92,4 +93,4 @@ "typescript": "5.7.2", "typescript-eslint": "8.16.0" } -} \ No newline at end of file +} diff --git a/src/index.test.ts b/src/index.test.ts index 96d6913..41c641a 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -20,25 +20,20 @@ describe('initializePlugin', () => { } as unknown as Matterbridge; mockLog = { error: jest.fn(), warn: jest.fn(), info: jest.fn(), debug: jest.fn() } as unknown as AnsiLogger; mockConfig = { - 'name': 'matterbridge-test', + 'name': 'matterbridge-somfy-tahoma', 'type': 'DynamicPlatform', 'username': 'None', 'password': 'None', 'service': 'somfy_europe', + 'blackList': [], + 'whiteList': [], 'debug': false, 'unregisterOnShutdown': false, } as PlatformConfig; }); - it('should return an instance of TestPlatform', () => { + it('should return an instance of SomfyTahomaPlatform', () => { const result = initializePlugin(mockMatterbridge, mockLog, mockConfig); - - expect(result).toBeInstanceOf(SomfyTahomaPlatform); - }); - - it('should shutdown the instance of TestPlatform', () => { - const result = initializePlugin(mockMatterbridge, mockLog, mockConfig); - expect(result).toBeInstanceOf(SomfyTahomaPlatform); }); }); diff --git a/src/platform.test.ts b/src/platform.test.ts index 469b3fc..6375d6d 100644 --- a/src/platform.test.ts +++ b/src/platform.test.ts @@ -1,44 +1,105 @@ -import { Matterbridge, PlatformConfig } from 'matterbridge'; -import { AnsiLogger } from 'matterbridge/logger'; +/* eslint-disable no-console */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Matterbridge, MatterbridgeDevice, MatterbridgeEndpoint, PlatformConfig } from 'matterbridge'; +import { AnsiLogger, dn, LogLevel, wr } from 'matterbridge/logger'; import { SomfyTahomaPlatform } from './platform'; + import { jest } from '@jest/globals'; +import { Client, Device } from 'overkiz-client'; describe('TestPlatform', () => { let mockMatterbridge: Matterbridge; let mockLog: AnsiLogger; let mockConfig: PlatformConfig; - let testPlatform: SomfyTahomaPlatform; + let somfyPlatform: SomfyTahomaPlatform; - // const log = new AnsiLogger({ logName: 'shellyDeviceTest', logTimestampFormat: TimestampFormat.TIME_MILLIS, logDebug: true }); + let clientConnectSpy: jest.SpiedFunction<(user: string, password: string) => Promise>; + let clientGetDevicesSpy: jest.SpiedFunction<(user: string, password: string) => Promise>; + let clientExecuteSpy: jest.SpiedFunction<(oid: any, execution: any) => Promise>; beforeAll(() => { + // Spy on the Client.connect method + clientConnectSpy = jest.spyOn(Client.prototype, 'connect').mockImplementation((user: string, password: string) => { + console.error(`Mocked Client.connect(${user}, ${password})`); + return Promise.resolve(); + }); + clientGetDevicesSpy = jest.spyOn(Client.prototype, 'getDevices').mockImplementation(() => { + console.error(`Mocked Client.getDevices()`); + return Promise.resolve([]); + }); + clientExecuteSpy = jest.spyOn(Client.prototype, 'execute').mockImplementation((oid: any, execution: any) => { + console.error(`Mocked Client.execute(${oid}, ${execution})`); + return Promise.resolve(); + }); + mockMatterbridge = { - addBridgedDevice: jest.fn(), matterbridgeDirectory: '', matterbridgePluginDirectory: 'temp', systemInformation: { ipv4Address: undefined }, matterbridgeVersion: '1.6.5', - removeAllBridgedDevices: jest.fn(), + addBridgedDevice: jest.fn(async (pluginName: string, device: MatterbridgeDevice) => { + // console.error('addBridgedDevice called'); + }), + addBridgedEndpoint: jest.fn(async (pluginName: string, device: MatterbridgeEndpoint) => { + device.number = 100; + // console.error('addBridgedEndpoint called'); + }), + removeBridgedDevice: jest.fn(async (pluginName: string, device: MatterbridgeDevice) => { + // console.error('removeBridgedDevice called'); + }), + removeBridgedEndpoint: jest.fn(async (pluginName: string, device: MatterbridgeEndpoint) => { + // console.error('removeBridgedEndpoint called'); + }), + removeAllBridgedDevices: jest.fn(async (pluginName: string) => { + // console.error('removeAllBridgedDevices called'); + }), + removeAllBridgedEndpoints: jest.fn(async (pluginName: string) => { + // console.error('removeAllBridgedEndpoints called'); + }), } as unknown as Matterbridge; - mockLog = { error: jest.fn(), warn: jest.fn(), info: jest.fn(), debug: jest.fn() } as unknown as AnsiLogger; + mockLog = { + fatal: jest.fn((message: string, ...parameters: any[]) => { + console.error('mockLog.fatal', message, ...parameters); + }), + error: jest.fn((message: string, ...parameters: any[]) => { + console.error('mockLog.error', message, ...parameters); + }), + warn: jest.fn((message: string, ...parameters: any[]) => { + console.error('mockLog.warn', message, ...parameters); + }), + notice: jest.fn((message: string, ...parameters: any[]) => { + console.error('mockLog.notice', message, ...parameters); + }), + info: jest.fn((message: string, ...parameters: any[]) => { + console.error('mockLog.info', message, ...parameters); + }), + debug: jest.fn((message: string, ...parameters: any[]) => { + console.error('mockLog.debug', message, ...parameters); + }), + } as unknown as AnsiLogger; mockConfig = { - 'name': 'matterbridge-test', + 'name': 'matterbridge-somfy-tahoma', 'type': 'DynamicPlatform', 'username': 'None', 'password': 'None', 'service': 'somfy_europe', + 'blackList': [], + 'whiteList': [], 'debug': false, 'unregisterOnShutdown': false, } as PlatformConfig; + }); - // testPlatform = new SomfyTahomaPlatform(mockMatterbridge, mockLog, mockConfig); + beforeEach(() => { + jest.clearAllMocks(); }); it('should not initialize platform without username and password', () => { mockConfig.username = undefined; mockConfig.password = undefined; mockConfig.service = undefined; - testPlatform = new SomfyTahomaPlatform(mockMatterbridge, mockLog, mockConfig); + somfyPlatform = new SomfyTahomaPlatform(mockMatterbridge, mockLog, mockConfig); expect(mockLog.info).toHaveBeenCalledWith('Initializing platform:', mockConfig.name); expect(mockLog.error).toHaveBeenCalledWith('No service or username or password provided for:', mockConfig.name); }); @@ -47,23 +108,116 @@ describe('TestPlatform', () => { mockConfig.username = 'None'; mockConfig.password = 'None'; mockConfig.service = 'somfy_europe'; - testPlatform = new SomfyTahomaPlatform(mockMatterbridge, mockLog, mockConfig); + somfyPlatform = new SomfyTahomaPlatform(mockMatterbridge, mockLog, mockConfig); expect(mockLog.info).toHaveBeenCalledWith('Initializing platform:', mockConfig.name); expect(mockLog.info).toHaveBeenCalledWith('Finished initializing platform:', mockConfig.name); + expect(mockLog.info).toHaveBeenCalledWith('Starting client Tahoma service somfy_europe with user None password: None'); + }); + + it('should return false and log a warning if entity is not in the whitelist', () => { + (somfyPlatform as any).whiteList = ['entity1', 'entity2']; + (somfyPlatform as any).blackList = []; + + const result = (somfyPlatform as any).validateWhiteBlackList('entity3'); + + expect(result).toBe(false); + expect(mockLog.warn).toHaveBeenCalledWith(`Skipping ${dn}entity3${wr} because not in whitelist`); + }); + + it('should return false and log a warning if entity is in the blacklist', () => { + (somfyPlatform as any).whiteList = []; + (somfyPlatform as any).blackList = ['entity3']; + + const result = (somfyPlatform as any).validateWhiteBlackList('entity3'); + + expect(result).toBe(false); + expect(mockLog.warn).toHaveBeenCalledWith(`Skipping ${dn}entity3${wr} because in blacklist`); + }); + + it('should return true if entity is in the whitelist', () => { + (somfyPlatform as any).whiteList = ['entity3']; + (somfyPlatform as any).blackList = []; + + const result = (somfyPlatform as any).validateWhiteBlackList('entity3'); + + expect(result).toBe(true); + expect(mockLog.warn).not.toHaveBeenCalled(); + }); + + it('should return true if entity is not in the blacklist and whitelist is empty', () => { + (somfyPlatform as any).whiteList = []; + (somfyPlatform as any).blackList = []; + + const result = (somfyPlatform as any).validateWhiteBlackList('entity3'); + + expect(result).toBe(true); + expect(mockLog.warn).not.toHaveBeenCalled(); + }); + + it('should return true if both whitelist and blacklist are empty', () => { + (somfyPlatform as any).whiteList = []; + (somfyPlatform as any).blackList = []; + + const result = (somfyPlatform as any).validateWhiteBlackList('entity3'); + + expect(result).toBe(true); + expect(mockLog.warn).not.toHaveBeenCalled(); + }); + + it('should validate version', () => { + mockMatterbridge.matterbridgeVersion = '1.5.4'; + expect(somfyPlatform.verifyMatterbridgeVersion('1.5.3')).toBe(true); + expect(somfyPlatform.verifyMatterbridgeVersion('1.5.4')).toBe(true); + expect(somfyPlatform.verifyMatterbridgeVersion('2.0.0')).toBe(false); + }); + + it('should validate version beta', () => { + mockMatterbridge.matterbridgeVersion = '1.5.4-dev.1'; + expect(somfyPlatform.verifyMatterbridgeVersion('1.5.3')).toBe(true); + expect(somfyPlatform.verifyMatterbridgeVersion('1.5.4')).toBe(true); + expect(somfyPlatform.verifyMatterbridgeVersion('2.0.0')).toBe(false); + mockMatterbridge.matterbridgeVersion = '1.5.5'; + }); + + it('should throw because of version', () => { + mockMatterbridge.matterbridgeVersion = '1.5.4'; + expect(() => new SomfyTahomaPlatform(mockMatterbridge, mockLog, mockConfig)).toThrow(); + mockMatterbridge.matterbridgeVersion = '1.6.5'; }); it('should call onStart with reason', async () => { - await testPlatform.onStart('Test reason'); + await somfyPlatform.onStart('Test reason'); + expect(mockLog.info).toHaveBeenCalledWith('onStart called with reason:', 'Test reason'); + expect(clientConnectSpy).toHaveBeenCalledWith('None', 'None'); + }); + + it('should call onStart with reason and log error', async () => { + const client = (somfyPlatform as any).tahomaClient; + (somfyPlatform as any).tahomaClient = undefined; + await somfyPlatform.onStart('Test reason'); + expect(mockLog.error).toHaveBeenCalledWith('TaHoma service not created'); + expect(clientConnectSpy).not.toHaveBeenCalledWith('None', 'None'); + (somfyPlatform as any).tahomaClient = client; + }); + + it('should call onStart with reason and log error if connect throws', async () => { + clientConnectSpy.mockImplementationOnce(() => { + throw new Error('Error connecting to TaHoma service'); + }); + await somfyPlatform.onStart('Test reason'); expect(mockLog.info).toHaveBeenCalledWith('onStart called with reason:', 'Test reason'); + expect(mockLog.error).not.toHaveBeenCalledWith('TaHoma service not created'); + expect(mockLog.error).toHaveBeenCalledWith(expect.stringContaining('Error connecting to TaHoma service'), undefined); + expect(clientConnectSpy).toHaveBeenCalledWith('None', 'None'); }); it('should call onConfigure', async () => { - await testPlatform.onConfigure(); + await somfyPlatform.onConfigure(); expect(mockLog.info).toHaveBeenCalledWith('onConfigure called'); }); it('should call onShutdown with reason', async () => { - await testPlatform.onShutdown('Test reason'); + await somfyPlatform.onShutdown('Test reason'); expect(mockLog.info).toHaveBeenCalledWith('onShutdown called with reason:', 'Test reason'); }); }); diff --git a/src/platform.ts b/src/platform.ts index 3b62729..3d92141 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -13,13 +13,13 @@ import { MatterbridgeEndpoint, coverDevice, } from 'matterbridge'; -import { AnsiLogger, BLUE, debugStringify, dn, rs, wr } from 'matterbridge/logger'; -import { isValidNumber } from 'matterbridge/utils'; +import { AnsiLogger, BLUE, debugStringify, dn, rs, wr, CYAN, db, ign, nf, YELLOW } from 'matterbridge/logger'; import { NodeStorageManager } from 'matterbridge/storage'; +import { isValidNumber } from 'matterbridge/utils'; import { Action, Client, Command, Device, Execution } from 'overkiz-client'; import path from 'path'; -import { CYAN, db, ign, nf, YELLOW } from 'node-ansi-logger'; +import { promises as fs } from 'fs'; type MovementDuration = Record; const Stopped = WindowCovering.MovementStatus.Stopped; @@ -114,7 +114,7 @@ export class SomfyTahomaPlatform extends MatterbridgeDynamicPlatform { override async onStart(reason?: string) { this.log.info('onStart called with reason:', reason ?? 'none'); if (!this.tahomaClient) { - this.log.error('TaHoma service not connected'); + this.log.error('TaHoma service not created'); return; } try { @@ -191,6 +191,19 @@ export class SomfyTahomaPlatform extends MatterbridgeDynamicPlatform { this.log.info('TaHoma', devices.length, 'devices discovered'); + // Create the plugin directory inside the Matterbridge plugin directory + await fs.mkdir(path.join(this.matterbridge.matterbridgePluginDirectory, 'matterbridge-somfy-tahoma'), { recursive: true }); + + // Write the discovered devices to a file + const fileName = path.join(this.matterbridge.matterbridgePluginDirectory, 'matterbridge-somfy-tahoma', 'devices.json'); + fs.writeFile(fileName, JSON.stringify(devices, null, 2)) + .then(() => { + this.log.debug(`Devices successfully written to ${fileName}`); + }) + .catch((error) => { + this.log.error(`Error writing devices to ${fileName}:`, error); + }); + for (const device of devices) { this.log.debug(`Device: ${BLUE}${device.label}${rs}`); this.log.debug(`- uniqueName ${device.uniqueName}`); @@ -199,7 +212,6 @@ export class SomfyTahomaPlatform extends MatterbridgeDynamicPlatform { this.log.debug(`- deviceURL ${device.deviceURL}`); this.log.debug(`- commands ${debugStringify(device.commands)}`); this.log.debug(`- states ${debugStringify(device.states)}`); - // this.log.debug(`Device: ${device.label} uniqueName ${device.uniqueName} uiClass ${device.definition.uiClass} deviceURL ${device.deviceURL} serial ${device.serialNumber}`); const supportedUniqueNames = [ 'Blind', 'BlindRTSComponent', diff --git a/tsconfig.production.json b/tsconfig.production.json index aace5f8..254ef2f 100644 --- a/tsconfig.production.json +++ b/tsconfig.production.json @@ -8,12 +8,6 @@ "sourceMap": false, "removeComments": true }, - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "**/*.spec.ts", - "**/*.test.ts", - "**/__test__/*" - ] -} \ No newline at end of file + "include": ["src/**/*.ts"], + "exclude": ["**/*.spec.ts", "**/*.test.ts", "**/__test__/*"] +}