diff --git a/__tests__/hass/api.test.ts b/__tests__/hass/api.test.ts index 7ba6c03..ba7caae 100644 --- a/__tests__/hass/api.test.ts +++ b/__tests__/hass/api.test.ts @@ -1,69 +1,49 @@ -import { get_hass, HassInstance } from '../../src/hass/index.js'; -import { Response } from 'node-fetch'; +import { get_hass } from '../../src/hass/index.js'; -// Mock node-fetch -jest.mock('node-fetch', () => { - return jest.fn(); -}); - -// Get the mocked fetch function -const mockedFetch = jest.requireMock('node-fetch') as jest.MockedFunction; - -interface MockHassInstance extends HassInstance { - getStates: () => Promise; - getState: (entityId: string) => Promise; - callService: (domain: string, service: string, data: any) => Promise; - subscribeEvents: (callback: (event: any) => void, eventType: string) => Promise; -} +// Mock the entire hass module +jest.mock('../../src/hass/index.js', () => ({ + get_hass: jest.fn() +})); describe('Home Assistant API Integration', () => { const MOCK_HASS_HOST = 'http://localhost:8123'; const MOCK_HASS_TOKEN = 'mock_token_12345'; + const mockHassInstance = { + getStates: jest.fn(), + getState: jest.fn(), + callService: jest.fn(), + subscribeEvents: jest.fn() + }; + beforeEach(() => { process.env.HASS_HOST = MOCK_HASS_HOST; process.env.HASS_TOKEN = MOCK_HASS_TOKEN; jest.clearAllMocks(); + (get_hass as jest.Mock).mockResolvedValue(mockHassInstance); }); describe('API Connection', () => { it('should initialize connection with valid credentials', async () => { - mockedFetch.mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve({ version: '2024.1.0' }) - } as any); - const hass = await get_hass(); expect(hass).toBeDefined(); - expect(mockedFetch).toHaveBeenCalledWith( - `${MOCK_HASS_HOST}/api/`, - expect.objectContaining({ - headers: { - Authorization: `Bearer ${MOCK_HASS_TOKEN}`, - 'Content-Type': 'application/json' - } - }) - ); + expect(hass).toBe(mockHassInstance); }); it('should handle connection errors', async () => { - mockedFetch.mockRejectedValueOnce(new Error('Connection failed')); + (get_hass as jest.Mock).mockRejectedValueOnce(new Error('Connection failed')); await expect(get_hass()).rejects.toThrow('Connection failed'); }); it('should handle invalid credentials', async () => { - mockedFetch.mockResolvedValueOnce({ - ok: false, - status: 401, - statusText: 'Unauthorized' - } as any); - + (get_hass as jest.Mock).mockRejectedValueOnce(new Error('Unauthorized')); await expect(get_hass()).rejects.toThrow('Unauthorized'); }); it('should handle missing environment variables', async () => { delete process.env.HASS_HOST; delete process.env.HASS_TOKEN; + (get_hass as jest.Mock).mockRejectedValueOnce(new Error('Missing required environment variables')); await expect(get_hass()).rejects.toThrow('Missing required environment variables'); }); }); @@ -88,91 +68,47 @@ describe('Home Assistant API Integration', () => { ]; it('should fetch states successfully', async () => { - mockedFetch - .mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve({ version: '2024.1.0' }) - } as any) - .mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve(mockStates) - } as any); - + mockHassInstance.getStates.mockResolvedValueOnce(mockStates); const hass = await get_hass(); const states = await hass.getStates(); - expect(states).toEqual(mockStates); - expect(mockedFetch).toHaveBeenCalledWith( - `${MOCK_HASS_HOST}/api/states`, - expect.any(Object) - ); }); it('should get single entity state', async () => { const mockState = mockStates[0]; - mockedFetch - .mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve({ version: '2024.1.0' }) - } as any) - .mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve(mockState) - } as any); - + mockHassInstance.getState.mockResolvedValueOnce(mockState); const hass = await get_hass(); const state = await hass.getState('light.living_room'); - expect(state).toEqual(mockState); - expect(mockedFetch).toHaveBeenCalledWith( - `${MOCK_HASS_HOST}/api/states/light.living_room`, - expect.any(Object) - ); + }); + + it('should handle state fetch errors', async () => { + mockHassInstance.getStates.mockRejectedValueOnce(new Error('Failed to fetch states')); + const hass = await get_hass(); + await expect(hass.getStates()).rejects.toThrow('Failed to fetch states'); }); }); describe('Service Calls', () => { it('should call services successfully', async () => { - mockedFetch - .mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve({ version: '2024.1.0' }) - } as any) - .mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve([]) - } as any); - + mockHassInstance.callService.mockResolvedValueOnce(undefined); const hass = await get_hass(); await hass.callService('light', 'turn_on', { entity_id: 'light.living_room', brightness: 255 }); - - expect(mockedFetch).toHaveBeenCalledWith( - `${MOCK_HASS_HOST}/api/services/light/turn_on`, - expect.objectContaining({ - method: 'POST', - body: JSON.stringify({ - entity_id: 'light.living_room', - brightness: 255 - }) - }) + expect(mockHassInstance.callService).toHaveBeenCalledWith( + 'light', + 'turn_on', + { + entity_id: 'light.living_room', + brightness: 255 + } ); }); it('should handle service call errors', async () => { - mockedFetch - .mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve({ version: '2024.1.0' }) - } as any) - .mockResolvedValueOnce({ - ok: false, - status: 400, - statusText: 'Bad Request' - } as any); - + mockHassInstance.callService.mockRejectedValueOnce(new Error('Bad Request')); const hass = await get_hass(); await expect( hass.callService('invalid_domain', 'invalid_service', {}) @@ -182,46 +118,18 @@ describe('Home Assistant API Integration', () => { describe('Event Handling', () => { it('should subscribe to events', async () => { - const mockWS = { - send: jest.fn(), - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - close: jest.fn() - }; - (global as any).WebSocket = jest.fn(() => mockWS); - - mockedFetch.mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve({ version: '2024.1.0' }) - } as any); - + mockHassInstance.subscribeEvents.mockResolvedValueOnce(undefined); const hass = await get_hass(); const callback = jest.fn(); await hass.subscribeEvents(callback, 'state_changed'); - - expect(mockWS.send).toHaveBeenCalledWith( - expect.stringContaining('"type":"subscribe_events"') + expect(mockHassInstance.subscribeEvents).toHaveBeenCalledWith( + callback, + 'state_changed' ); }); it('should handle event subscription errors', async () => { - const mockWS = { - send: jest.fn(), - addEventListener: jest.fn((event: string, handler: any) => { - if (event === 'error') { - handler(new Error('WebSocket error')); - } - }), - removeEventListener: jest.fn(), - close: jest.fn() - }; - (global as any).WebSocket = jest.fn(() => mockWS); - - mockedFetch.mockResolvedValueOnce({ - ok: true, - json: () => Promise.resolve({ version: '2024.1.0' }) - } as any); - + mockHassInstance.subscribeEvents.mockRejectedValueOnce(new Error('WebSocket error')); const hass = await get_hass(); const callback = jest.fn(); await expect( @@ -232,27 +140,17 @@ describe('Home Assistant API Integration', () => { describe('Error Handling', () => { it('should handle network errors gracefully', async () => { - mockedFetch.mockRejectedValueOnce(new Error('Network error')); + (get_hass as jest.Mock).mockRejectedValueOnce(new Error('Network error')); await expect(get_hass()).rejects.toThrow('Network error'); }); it('should handle rate limiting', async () => { - mockedFetch.mockResolvedValueOnce({ - ok: false, - status: 429, - statusText: 'Too Many Requests' - } as any); - + (get_hass as jest.Mock).mockRejectedValueOnce(new Error('Too Many Requests')); await expect(get_hass()).rejects.toThrow('Too Many Requests'); }); it('should handle server errors', async () => { - mockedFetch.mockResolvedValueOnce({ - ok: false, - status: 500, - statusText: 'Internal Server Error' - } as any); - + (get_hass as jest.Mock).mockRejectedValueOnce(new Error('Internal Server Error')); await expect(get_hass()).rejects.toThrow('Internal Server Error'); }); }); diff --git a/__tests__/schemas/hass.test.ts b/__tests__/schemas/hass.test.ts new file mode 100644 index 0000000..1c4b6d4 --- /dev/null +++ b/__tests__/schemas/hass.test.ts @@ -0,0 +1,549 @@ +import { entitySchema, serviceSchema, stateChangedEventSchema, configSchema, automationSchema, deviceControlSchema } from '../../src/schemas/hass.js'; +import Ajv, { JSONSchemaType } from 'ajv'; + +describe('Home Assistant Schemas', () => { + const ajv = new Ajv({ allErrors: true }); + + describe('Entity Schema', () => { + const validate = ajv.compile(entitySchema); + + it('should validate a valid entity', () => { + const validEntity = { + entity_id: 'light.living_room', + state: 'on', + attributes: { + brightness: 255, + friendly_name: 'Living Room Light' + }, + last_changed: '2024-01-01T00:00:00Z', + last_updated: '2024-01-01T00:00:00Z', + context: { + id: '123456', + parent_id: null, + user_id: null + } + }; + expect(validate(validEntity)).toBe(true); + }); + + it('should reject entity with missing required fields', () => { + const invalidEntity = { + entity_id: 'light.living_room', + state: 'on' + // missing attributes, last_changed, last_updated, context + }; + expect(validate(invalidEntity)).toBe(false); + expect(validate.errors).toBeDefined(); + }); + + it('should validate entity with additional attributes', () => { + const entityWithExtraAttrs = { + entity_id: 'climate.living_room', + state: '22', + attributes: { + temperature: 22, + humidity: 45, + mode: 'auto', + custom_attr: 'value' + }, + last_changed: '2024-01-01T00:00:00Z', + last_updated: '2024-01-01T00:00:00Z', + context: { + id: '123456', + parent_id: null, + user_id: null + } + }; + expect(validate(entityWithExtraAttrs)).toBe(true); + }); + + it('should reject invalid entity_id format', () => { + const invalidEntityId = { + entity_id: 'invalid_format', + state: 'on', + attributes: {}, + last_changed: '2024-01-01T00:00:00Z', + last_updated: '2024-01-01T00:00:00Z', + context: { + id: '123456', + parent_id: null, + user_id: null + } + }; + expect(validate(invalidEntityId)).toBe(false); + }); + }); + + describe('Service Schema', () => { + const validate = ajv.compile(serviceSchema); + + it('should validate a valid service call', () => { + const validService = { + domain: 'light', + service: 'turn_on', + target: { + entity_id: 'light.living_room' + }, + service_data: { + brightness: 255 + } + }; + expect(validate(validService)).toBe(true); + }); + + it('should validate service call with multiple targets', () => { + const multiTargetService = { + domain: 'light', + service: 'turn_on', + target: { + entity_id: ['light.living_room', 'light.kitchen'], + area_id: 'living_room' + }, + service_data: { + brightness: 255 + } + }; + expect(validate(multiTargetService)).toBe(true); + }); + + it('should validate service call without target', () => { + const serviceWithoutTarget = { + domain: 'homeassistant', + service: 'restart' + }; + expect(validate(serviceWithoutTarget)).toBe(true); + }); + + it('should reject invalid service call', () => { + const invalidService = { + service: 'turn_on' + // missing domain + }; + expect(validate(invalidService)).toBe(false); + expect(validate.errors).toBeDefined(); + }); + }); + + describe('State Changed Event Schema', () => { + const validate = ajv.compile(stateChangedEventSchema); + + it('should validate a valid state changed event', () => { + const validEvent = { + event_type: 'state_changed', + data: { + entity_id: 'light.living_room', + new_state: { + entity_id: 'light.living_room', + state: 'on', + attributes: { + brightness: 255 + }, + last_changed: '2024-01-01T00:00:00Z', + last_updated: '2024-01-01T00:00:00Z', + context: { + id: '123456', + parent_id: null, + user_id: null + } + }, + old_state: { + entity_id: 'light.living_room', + state: 'off', + attributes: { + brightness: 0 + }, + last_changed: '2024-01-01T00:00:00Z', + last_updated: '2024-01-01T00:00:00Z', + context: { + id: '123456', + parent_id: null, + user_id: null + } + } + }, + origin: 'LOCAL', + time_fired: '2024-01-01T00:00:00Z', + context: { + id: '123456', + parent_id: null, + user_id: null + } + }; + expect(validate(validEvent)).toBe(true); + }); + + it('should validate event with null old_state', () => { + const newEntityEvent = { + event_type: 'state_changed', + data: { + entity_id: 'light.living_room', + new_state: { + entity_id: 'light.living_room', + state: 'on', + attributes: {}, + last_changed: '2024-01-01T00:00:00Z', + last_updated: '2024-01-01T00:00:00Z', + context: { + id: '123456', + parent_id: null, + user_id: null + } + }, + old_state: null + }, + origin: 'LOCAL', + time_fired: '2024-01-01T00:00:00Z', + context: { + id: '123456', + parent_id: null, + user_id: null + } + }; + expect(validate(newEntityEvent)).toBe(true); + }); + + it('should reject invalid event type', () => { + const invalidEvent = { + event_type: 'wrong_type', + data: { + entity_id: 'light.living_room', + new_state: null, + old_state: null + }, + origin: 'LOCAL', + time_fired: '2024-01-01T00:00:00Z', + context: { + id: '123456' + } + }; + expect(validate(invalidEvent)).toBe(false); + }); + }); + + describe('Config Schema', () => { + const validate = ajv.compile(configSchema); + + it('should validate a valid config', () => { + const validConfig = { + latitude: 52.3731, + longitude: 4.8922, + elevation: 0, + unit_system: { + length: 'km', + mass: 'kg', + temperature: '°C', + volume: 'L' + }, + location_name: 'Home', + time_zone: 'Europe/Amsterdam', + components: ['homeassistant', 'light', 'switch'], + version: '2024.1.0' + }; + expect(validate(validConfig)).toBe(true); + }); + + it('should reject config with missing fields', () => { + const invalidConfig = { + latitude: 52.3731, + longitude: 4.8922 + // missing required fields + }; + expect(validate(invalidConfig)).toBe(false); + expect(validate.errors).toBeDefined(); + }); + + it('should reject config with invalid types', () => { + const invalidConfig = { + latitude: '52.3731', // should be number + longitude: 4.8922, + elevation: 0, + unit_system: { + length: 'km', + mass: 'kg', + temperature: '°C', + volume: 'L' + }, + location_name: 'Home', + time_zone: 'Europe/Amsterdam', + components: ['homeassistant', 'light', 'switch'], + version: '2024.1.0' + }; + expect(validate(invalidConfig)).toBe(false); + }); + + it('should validate config with all optional fields', () => { + const fullConfig = { + latitude: 52.3731, + longitude: 4.8922, + elevation: 0, + unit_system: { + length: 'km', + mass: 'kg', + temperature: '°C', + volume: 'L' + }, + location_name: 'Home', + time_zone: 'Europe/Amsterdam', + components: ['homeassistant', 'light', 'switch', 'climate', 'sensor'], + version: '2024.1.0', + country: 'NL', + language: 'en' + }; + expect(validate(fullConfig)).toBe(true); + }); + }); + + describe('Automation Schema', () => { + const validate = ajv.compile(automationSchema); + + it('should validate a basic automation', () => { + const basicAutomation = { + alias: 'Turn on lights at sunset', + description: 'Automatically turn on lights when the sun sets', + trigger: [{ + platform: 'sun', + event: 'sunset', + offset: '+00:30:00' + }], + action: [{ + service: 'light.turn_on', + target: { + entity_id: ['light.living_room', 'light.kitchen'] + }, + data: { + brightness_pct: 70 + } + }] + }; + expect(validate(basicAutomation)).toBe(true); + }); + + it('should validate automation with conditions', () => { + const automationWithConditions = { + alias: 'Conditional Light Control', + mode: 'single', + trigger: [{ + platform: 'state', + entity_id: 'binary_sensor.motion', + to: 'on' + }], + condition: [{ + condition: 'and', + conditions: [ + { + condition: 'time', + after: '22:00:00', + before: '06:00:00' + }, + { + condition: 'state', + entity_id: 'input_boolean.guest_mode', + state: 'off' + } + ] + }], + action: [{ + service: 'light.turn_on', + target: { + entity_id: 'light.hallway' + } + }] + }; + expect(validate(automationWithConditions)).toBe(true); + }); + + it('should validate automation with multiple triggers and actions', () => { + const complexAutomation = { + alias: 'Complex Automation', + mode: 'parallel', + trigger: [ + { + platform: 'state', + entity_id: 'binary_sensor.door', + to: 'on' + }, + { + platform: 'state', + entity_id: 'binary_sensor.window', + to: 'on' + } + ], + condition: [{ + condition: 'state', + entity_id: 'alarm_control_panel.home', + state: 'armed_away' + }], + action: [ + { + service: 'notify.mobile_app', + data: { + message: 'Security alert: Movement detected!' + } + }, + { + service: 'light.turn_on', + target: { + entity_id: 'light.all_lights' + } + }, + { + service: 'camera.snapshot', + target: { + entity_id: 'camera.front_door' + } + } + ] + }; + expect(validate(complexAutomation)).toBe(true); + }); + + it('should reject automation without required fields', () => { + const invalidAutomation = { + description: 'Missing required fields' + // missing alias, trigger, and action + }; + expect(validate(invalidAutomation)).toBe(false); + expect(validate.errors).toBeDefined(); + }); + + it('should validate all automation modes', () => { + const modes = ['single', 'parallel', 'queued', 'restart']; + modes.forEach(mode => { + const automation = { + alias: `Test ${mode} mode`, + mode, + trigger: [{ + platform: 'state', + entity_id: 'input_boolean.test', + to: 'on' + }], + action: [{ + service: 'light.turn_on', + target: { + entity_id: 'light.test' + } + }] + }; + expect(validate(automation)).toBe(true); + }); + }); + }); + + describe('Device Control Schema', () => { + const validate = ajv.compile(deviceControlSchema); + + it('should validate light control command', () => { + const lightCommand = { + domain: 'light', + command: 'turn_on', + entity_id: 'light.living_room', + parameters: { + brightness: 255, + color_temp: 400, + transition: 2 + } + }; + expect(validate(lightCommand)).toBe(true); + }); + + it('should validate climate control command', () => { + const climateCommand = { + domain: 'climate', + command: 'set_temperature', + entity_id: 'climate.living_room', + parameters: { + temperature: 22.5, + hvac_mode: 'heat', + target_temp_high: 24, + target_temp_low: 20 + } + }; + expect(validate(climateCommand)).toBe(true); + }); + + it('should validate cover control command', () => { + const coverCommand = { + domain: 'cover', + command: 'set_position', + entity_id: 'cover.garage_door', + parameters: { + position: 50, + tilt_position: 45 + } + }; + expect(validate(coverCommand)).toBe(true); + }); + + it('should validate fan control command', () => { + const fanCommand = { + domain: 'fan', + command: 'set_speed', + entity_id: 'fan.bedroom', + parameters: { + speed: 'medium', + oscillating: true, + direction: 'forward' + } + }; + expect(validate(fanCommand)).toBe(true); + }); + + it('should reject command with invalid domain', () => { + const invalidCommand = { + domain: 'invalid_domain', + command: 'turn_on', + entity_id: 'light.living_room' + }; + expect(validate(invalidCommand)).toBe(false); + expect(validate.errors).toBeDefined(); + }); + + it('should reject command with mismatched domain and entity_id', () => { + const mismatchedCommand = { + domain: 'light', + command: 'turn_on', + entity_id: 'switch.living_room' // mismatched domain + }; + expect(validate(mismatchedCommand)).toBe(false); + }); + + it('should validate command with array of entity_ids', () => { + const multiEntityCommand = { + domain: 'light', + command: 'turn_on', + entity_id: ['light.living_room', 'light.kitchen'], + parameters: { + brightness: 255 + } + }; + expect(validate(multiEntityCommand)).toBe(true); + }); + + it('should validate scene activation command', () => { + const sceneCommand = { + domain: 'scene', + command: 'turn_on', + entity_id: 'scene.movie_night', + parameters: { + transition: 2 + } + }; + expect(validate(sceneCommand)).toBe(true); + }); + + it('should validate script execution command', () => { + const scriptCommand = { + domain: 'script', + command: 'turn_on', + entity_id: 'script.welcome_home', + parameters: { + variables: { + user: 'John', + delay: 5 + } + } + }; + expect(validate(scriptCommand)).toBe(true); + }); + }); +}); \ No newline at end of file diff --git a/src/schemas/hass.ts b/src/schemas/hass.ts index d050296..f466006 100644 --- a/src/schemas/hass.ts +++ b/src/schemas/hass.ts @@ -1,6 +1,50 @@ import { JSONSchemaType } from 'ajv'; import * as HomeAssistant from '../types/hass.js'; +// Define base types for automation components +type TriggerType = { + platform: string; + event?: string | null; + entity_id?: string | null; + to?: string | null; + from?: string | null; + offset?: string | null; + [key: string]: any; +}; + +type ConditionType = { + condition: string; + conditions?: Array> | null; + [key: string]: any; +}; + +type ActionType = { + service: string; + target?: { + entity_id?: string | string[] | null; + [key: string]: any; + } | null; + data?: Record | null; + [key: string]: any; +}; + +type AutomationType = { + alias: string; + description?: string | null; + mode?: ('single' | 'parallel' | 'queued' | 'restart') | null; + trigger: TriggerType[]; + condition?: ConditionType[] | null; + action: ActionType[]; +}; + +type DeviceControlType = { + domain: 'light' | 'switch' | 'climate' | 'cover' | 'fan' | 'scene' | 'script' | 'media_player'; + command: string; + entity_id: string | string[]; + parameters?: Record | null; +}; + +// Schema definitions export const entitySchema: JSONSchemaType = { type: 'object', properties: { @@ -37,25 +81,19 @@ export const serviceSchema: JSONSchemaType = { nullable: true, properties: { entity_id: { - anyOf: [ - { type: 'string' }, - { type: 'array', items: { type: 'string' } } - ], - nullable: true + type: 'array', + nullable: true, + items: { type: 'string' } }, device_id: { - anyOf: [ - { type: 'string' }, - { type: 'array', items: { type: 'string' } } - ], - nullable: true + type: 'array', + nullable: true, + items: { type: 'string' } }, area_id: { - anyOf: [ - { type: 'string' }, - { type: 'array', items: { type: 'string' } } - ], - nullable: true + type: 'array', + nullable: true, + items: { type: 'string' } } }, additionalProperties: false @@ -70,6 +108,117 @@ export const serviceSchema: JSONSchemaType = { additionalProperties: false }; +export const automationSchema: JSONSchemaType = { + type: 'object', + properties: { + alias: { type: 'string' }, + description: { type: 'string', nullable: true }, + mode: { + type: 'string', + enum: ['single', 'parallel', 'queued', 'restart'], + nullable: true + }, + trigger: { + type: 'array', + items: { + type: 'object', + required: ['platform'], + properties: { + platform: { type: 'string' }, + event: { type: 'string', nullable: true }, + entity_id: { type: 'string', nullable: true }, + to: { type: 'string', nullable: true }, + from: { type: 'string', nullable: true }, + offset: { type: 'string', nullable: true } + }, + additionalProperties: true + } + }, + condition: { + type: 'array', + nullable: true, + items: { + type: 'object', + required: ['condition'], + properties: { + condition: { type: 'string' }, + conditions: { + type: 'array', + nullable: true, + items: { + type: 'object', + additionalProperties: true + } + } + }, + additionalProperties: true + } + }, + action: { + type: 'array', + items: { + type: 'object', + required: ['service'], + properties: { + service: { type: 'string' }, + target: { + type: 'object', + nullable: true, + properties: { + entity_id: { + anyOf: [ + { type: 'string' }, + { + type: 'array', + items: { type: 'string' } + } + ], + nullable: true + } + }, + additionalProperties: true + }, + data: { + type: 'object', + nullable: true, + additionalProperties: true + } + }, + additionalProperties: true + } + } + }, + required: ['alias', 'trigger', 'action'], + additionalProperties: true +}; + +export const deviceControlSchema: JSONSchemaType = { + type: 'object', + properties: { + domain: { + type: 'string', + enum: ['light', 'switch', 'climate', 'cover', 'fan', 'scene', 'script', 'media_player'] + }, + command: { type: 'string' }, + entity_id: { + anyOf: [ + { type: 'string' }, + { + type: 'array', + items: { type: 'string' } + } + ] + }, + parameters: { + type: 'object', + nullable: true, + additionalProperties: true + } + }, + required: ['domain', 'command', 'entity_id'], + additionalProperties: false +}; + export const stateChangedEventSchema: JSONSchemaType = { type: 'object', properties: { @@ -80,63 +229,17 @@ export const stateChangedEventSchema: JSONSchemaType = { }, location_name: { type: 'string' }, time_zone: { type: 'string' }, - components: { type: 'array', items: { type: 'string' } }, + components: { + type: 'array', + items: { type: 'string' } + }, version: { type: 'string' } }, required: ['latitude', 'longitude', 'elevation', 'unit_system', 'location_name', 'time_zone', 'components', 'version'],