UNPKG

homebridge-tsvesync

Version:

Homebridge plugin for VeSync devices including Levoit air purifiers, humidifiers, and Etekcity smart outlets

528 lines (475 loc) 18.2 kB
import { API, Logger, Service as ServiceType, Characteristic as CharacteristicType } from 'homebridge'; import { VeSync } from 'tsvesync'; import { TSVESyncPlatform } from '../../platform'; import { TEST_CONFIG } from '../setup'; import { createMockLogger, createMockOutlet, createMockVeSync } from '../utils/test-helpers'; import { PLATFORM_NAME, PLUGIN_NAME } from '../../settings'; import { DeviceFactory } from '../../utils/device-factory'; import { BaseAccessory } from '../../accessories/base.accessory'; import { OutletAccessory } from '../../accessories/outlet.accessory'; import { VeSyncOutlet } from '../../types/device.types'; import { RetryManager } from '../../utils/retry'; // Mock RetryManager jest.mock('../../utils/retry'); const mockRetryManager = jest.mocked(RetryManager); // Import constants from outlet accessory const POWER_SERVICE_NAME = 'Power Consumption'; const POWER_CHARACTERISTIC = { displayName: 'Power', UUID: '7B2B25B0-DB50-4351-9A8B-5B9F3E3E3E3E', }; const VOLTAGE_CHARACTERISTIC = { displayName: 'Voltage', UUID: '7B2B25B1-DB50-4351-9A8B-5B9F3E3E3E3E', }; const ENERGY_CHARACTERISTIC = { displayName: 'Energy', UUID: '7B2B25B2-DB50-4351-9A8B-5B9F3E3E3E3E', }; jest.mock('../../utils/device-factory'); const mockDeviceFactory = jest.mocked(DeviceFactory); describe('Outlet Device Tests', () => { let platform: TSVESyncPlatform; let mockLogger: jest.Mocked<Logger>; let mockAPI: jest.Mocked<API>; let mockVeSync: jest.Mocked<VeSync>; const defaultConfig = { platform: 'TSVESync', name: 'TSVESync', username: TEST_CONFIG.username || 'test@example.com', password: TEST_CONFIG.password || 'password123', apiUrl: TEST_CONFIG.apiUrl, debug: true, }; beforeEach(() => { // Reset all mocks jest.clearAllMocks(); // Mock RetryManager to execute operations immediately mockRetryManager.prototype.execute.mockImplementation(async (operation) => { return operation(); }); mockLogger = createMockLogger(); // Setup API mock mockAPI = { version: 2.0, serverVersion: '1.0.0', user: { configPath: jest.fn(), storagePath: jest.fn().mockReturnValue('/tmp'), persistPath: jest.fn(), }, hapLegacyTypes: {}, platformAccessory: jest.fn().mockImplementation((name, uuid) => ({ UUID: uuid, displayName: name, context: {}, services: new Map(), addService: jest.fn().mockImplementation((service) => service), removeService: jest.fn(), getService: jest.fn().mockReturnValue({ getCharacteristic: jest.fn().mockReturnValue({ onSet: jest.fn(), onGet: jest.fn(), updateValue: jest.fn(), }), setCharacteristic: jest.fn().mockReturnThis(), }), getServiceById: jest.fn(), })), versionGreaterOrEqual: jest.fn(), registerAccessory: jest.fn(), registerPlatform: jest.fn(), publishCameraAccessories: jest.fn(), registerPlatformAccessories: jest.fn(), unregisterPlatformAccessories: jest.fn(), publishExternalAccessories: jest.fn(), updatePlatformAccessories: jest.fn(), registerPlatformAccessory: jest.fn(), on: jest.fn(), emit: jest.fn(), hap: { Service: { AccessoryInformation: jest.fn(), Outlet: jest.fn().mockImplementation(() => ({ getCharacteristic: jest.fn().mockReturnValue({ onSet: jest.fn(), onGet: jest.fn(), updateValue: jest.fn(), }), setCharacteristic: jest.fn().mockReturnThis(), })), AirPurifier: jest.fn(), HumiditySensor: jest.fn(), Fan: jest.fn(), Switch: jest.fn().mockImplementation(() => ({ getCharacteristic: jest.fn().mockReturnValue({ onSet: jest.fn(), onGet: jest.fn(), updateValue: jest.fn(), }), setCharacteristic: jest.fn().mockReturnThis(), })), } as unknown as typeof ServiceType, Characteristic: { On: 'On', Active: 'Active', Name: 'Name', Model: 'Model', Manufacturer: 'Manufacturer', SerialNumber: 'SerialNumber', FirmwareRevision: 'FirmwareRevision', OutletInUse: 'OutletInUse', Voltage: 'Voltage', ElectricCurrent: 'ElectricCurrent', PowerMeterVisible: 'PowerMeterVisible', } as unknown as typeof CharacteristicType, uuid: { generate: jest.fn().mockImplementation((id) => `test-uuid-${id}`), }, }, } as unknown as jest.Mocked<API>; // Setup VeSync mock mockVeSync = createMockVeSync(); // Initialize platform platform = new TSVESyncPlatform(mockLogger, defaultConfig, mockAPI); // Inject the mock VeSync client (platform as any).client = mockVeSync; // Mock DeviceFactory mockDeviceFactory.getAccessoryCategory.mockReturnValue(1); // 1 is the category for outlets mockDeviceFactory.createAccessory.mockImplementation((platform, accessory, device) => { const baseAccessory = { service: new mockAPI.hap.Service.Outlet(), platform, accessory, device, initialize: jest.fn().mockResolvedValue(undefined), applyUpdatedDeviceState: jest.fn(), updateCharacteristicValue: jest.fn(), } as unknown as BaseAccessory; return baseAccessory; }); }); describe('outlet state management', () => { it('should handle power state changes', async () => { const mockOutlet = createMockOutlet({ deviceName: 'Test Outlet', deviceType: 'wifi-switch-1.3', cid: 'test-cid-123', power: 10 // Power in watts }); // Setup VeSync client with the mock outlet mockVeSync.outlets = [mockOutlet]; mockVeSync.update.mockResolvedValue(undefined); mockVeSync.login.mockResolvedValue(true); // Initialize platform and wait for discovery await platform.discoverDevices(); expect(mockAPI.registerPlatformAccessories).toHaveBeenCalledWith( PLUGIN_NAME, PLATFORM_NAME, expect.arrayContaining([ expect.objectContaining({ UUID: expect.stringContaining('test-uuid-test-cid-123'), displayName: 'Test Outlet' }) ]) ); // Verify power state changes await mockOutlet.turnOff(); expect(mockOutlet.turnOff).toHaveBeenCalled(); }); it('should handle energy monitoring', async () => { const expectedDetails = { deviceStatus: 'on', power: 0, voltage: 120, energy: 0.5, }; // Create mock outlet and accessory const mockOutlet = createMockOutlet({ deviceName: 'Test Outlet', deviceType: 'wifi-switch-1.3', cid: 'test-cid-123', }); const accessory = new mockAPI.platformAccessory(mockOutlet.deviceName, mockAPI.hap.uuid.generate(mockOutlet.cid)); accessory.context.device = mockOutlet; // Mock getDetails to return expected values mockOutlet.getDetails = jest.fn().mockResolvedValue(expectedDetails); // Create mock services const mockBaseService = { getCharacteristic: jest.fn().mockImplementation((name) => { const characteristic = { onGet: jest.fn().mockImplementation((handler) => { characteristic.getValue = handler; return characteristic; }), onSet: jest.fn().mockImplementation((handler) => { characteristic.setValue = handler; return characteristic; }), getValue: jest.fn(), setValue: jest.fn(), updateValue: jest.fn(), }; return characteristic; }), addCharacteristic: jest.fn().mockImplementation((char) => char), }; const mockPowerService = { getCharacteristic: jest.fn().mockImplementation((uuid) => { const characteristic = { UUID: uuid, value: undefined, onGet: jest.fn().mockImplementation((handler) => { characteristic.getValue = handler; return characteristic; }), onSet: jest.fn().mockImplementation((handler) => { characteristic.setValue = handler; return characteristic; }), getValue: jest.fn(), setValue: jest.fn(), updateValue: jest.fn().mockImplementation((value) => { characteristic.value = value; return characteristic; }), }; return characteristic; }), addCharacteristic: jest.fn().mockImplementation((char) => { const characteristic = { UUID: char.UUID, value: undefined, onGet: jest.fn().mockImplementation((handler) => { characteristic.getValue = handler; return characteristic; }), onSet: jest.fn().mockImplementation((handler) => { characteristic.setValue = handler; return characteristic; }), getValue: jest.fn(), setValue: jest.fn(), updateValue: jest.fn().mockImplementation((value) => { characteristic.value = value; return characteristic; }), }; return characteristic; }), }; accessory.getService = jest.fn().mockImplementation((service) => { if (service === platform.Service.Outlet) { return mockBaseService; } if (service === 'Power Consumption') { return mockPowerService; } return undefined; }); // Create the outlet accessory const outlet = new OutletAccessory(platform, accessory, mockOutlet); // Wait for initial setup await new Promise(resolve => setTimeout(resolve, 100)); // Get the power service const powerService = accessory.getService('Power Consumption'); expect(powerService).toBeDefined(); // Get characteristics const powerChar = powerService?.getCharacteristic('7B2B25B0-DB50-4351-9A8B-5B9F3E3E3E3E'); const voltageChar = powerService?.getCharacteristic('7B2B25B1-DB50-4351-9A8B-5B9F3E3E3E3E'); const energyChar = powerService?.getCharacteristic('7B2B25B2-DB50-4351-9A8B-5B9F3E3E3E3E'); expect(powerChar).toBeDefined(); expect(voltageChar).toBeDefined(); expect(energyChar).toBeDefined(); if (!powerChar || !voltageChar || !energyChar) { throw new Error('Power monitoring characteristics not found'); } // Update characteristic values powerChar.updateValue(expectedDetails.power); voltageChar.updateValue(expectedDetails.voltage); energyChar.updateValue(expectedDetails.energy); // Verify values expect(powerChar.value).toBe(expectedDetails.power); expect(voltageChar.value).toBe(expectedDetails.voltage); expect(energyChar.value).toBe(expectedDetails.energy); }, 10000); // Increase timeout to 10 seconds it('should handle device errors', async () => { const mockOutlet = createMockOutlet({ deviceName: 'Test Outlet', deviceType: 'wifi-switch-1.3', cid: 'test-cid-123', }); const accessory = new mockAPI.platformAccessory(mockOutlet.deviceName, mockAPI.hap.uuid.generate(mockOutlet.cid)); accessory.context.device = mockOutlet; // Mock turnOn to throw an error mockOutlet.turnOn.mockRejectedValue(new Error('Failed to turn on')); // Mock the getService method to return a service with proper characteristics const mockOutletService = { getCharacteristic: jest.fn().mockImplementation((name) => { const characteristic = { onGet: jest.fn().mockImplementation((handler) => { characteristic.getValue = handler; return characteristic; }), onSet: jest.fn().mockImplementation((handler) => { characteristic.setValue = handler; return characteristic; }), getValue: jest.fn(), setValue: jest.fn().mockImplementation((value) => { if (value) { return Promise.reject(new Error('Failed to turn on')); } return Promise.resolve(); }), updateValue: jest.fn(), }; return characteristic; }), addCharacteristic: jest.fn().mockImplementation((char) => char), }; const mockPowerService = { getCharacteristic: jest.fn().mockImplementation((uuid) => { const characteristic = { UUID: uuid, value: undefined, onGet: jest.fn().mockImplementation((handler) => { characteristic.getValue = handler; return characteristic; }), onSet: jest.fn().mockImplementation((handler) => { characteristic.setValue = handler; return characteristic; }), getValue: jest.fn(), setValue: jest.fn(), updateValue: jest.fn().mockImplementation((value) => { characteristic.value = value; return characteristic; }), }; return characteristic; }), addCharacteristic: jest.fn().mockImplementation((char) => { const characteristic = { UUID: char.UUID, value: undefined, onGet: jest.fn().mockImplementation((handler) => { characteristic.getValue = handler; return characteristic; }), onSet: jest.fn().mockImplementation((handler) => { characteristic.setValue = handler; return characteristic; }), getValue: jest.fn(), setValue: jest.fn(), updateValue: jest.fn().mockImplementation((value) => { characteristic.value = value; return characteristic; }), }; return characteristic; }), }; accessory.getService = jest.fn().mockImplementation((service) => { if (service === platform.Service.Outlet) { return mockOutletService; } if (service === 'Power Consumption') { return mockPowerService; } return undefined; }); // Create the outlet accessory const outlet = new OutletAccessory(platform, accessory, mockOutlet); // Wait for initial setup await new Promise(resolve => setTimeout(resolve, 100)); // Get the outlet service const outletService = accessory.getService(platform.Service.Outlet); expect(outletService).toBeDefined(); // Get the On characteristic const onChar = outletService?.getCharacteristic('On'); expect(onChar).toBeDefined(); if (!onChar) { throw new Error('On characteristic not found'); } // Try to set the value and expect it to throw await expect(onChar.setValue(true)).rejects.toThrow('Failed to turn on'); }, 10000); // Increase timeout to 10 seconds }); describe('outlet device types', () => { it('should support ESO15-TB devices', async () => { const mockOutlet = createMockOutlet({ deviceName: 'Test Outlet', deviceType: 'ESO15-TB', cid: 'test-cid-123' }); // Setup VeSync client with the mock outlet mockVeSync.outlets = [mockOutlet]; mockVeSync.update.mockResolvedValue(undefined); mockVeSync.login.mockResolvedValue(true); // Initialize platform and wait for discovery await platform.discoverDevices(); expect(mockAPI.registerPlatformAccessories).toHaveBeenCalledWith( PLUGIN_NAME, PLATFORM_NAME, expect.arrayContaining([ expect.objectContaining({ UUID: expect.stringContaining('test-uuid-test-cid-123'), displayName: 'Test Outlet' }) ]) ); expect(mockOutlet.deviceType).toBe('ESO15-TB'); }); it('should support ESW03-USA devices', async () => { const mockOutlet = createMockOutlet({ deviceName: 'Test Outlet', deviceType: 'ESW03-USA', cid: 'test-cid-123' }); // Setup VeSync client with the mock outlet mockVeSync.outlets = [mockOutlet]; mockVeSync.update.mockResolvedValue(undefined); mockVeSync.login.mockResolvedValue(true); // Initialize platform and wait for discovery await platform.discoverDevices(); expect(mockAPI.registerPlatformAccessories).toHaveBeenCalledWith( PLUGIN_NAME, PLATFORM_NAME, expect.arrayContaining([ expect.objectContaining({ UUID: expect.stringContaining('test-uuid-test-cid-123'), displayName: 'Test Outlet' }) ]) ); expect(mockOutlet.deviceType).toBe('ESW03-USA'); }); it('should support wifi-switch-1.3 devices', async () => { const mockOutlet = createMockOutlet({ deviceName: 'Test Outlet', deviceType: 'wifi-switch-1.3', cid: 'test-cid-123' }); // Setup VeSync client with the mock outlet mockVeSync.outlets = [mockOutlet]; mockVeSync.update.mockResolvedValue(undefined); mockVeSync.login.mockResolvedValue(true); // Initialize platform and wait for discovery await platform.discoverDevices(); expect(mockAPI.registerPlatformAccessories).toHaveBeenCalledWith( PLUGIN_NAME, PLATFORM_NAME, expect.arrayContaining([ expect.objectContaining({ UUID: expect.stringContaining('test-uuid-test-cid-123'), displayName: 'Test Outlet' }) ]) ); expect(mockOutlet.deviceType).toBe('wifi-switch-1.3'); }); }); });