Skip to content

Commit

Permalink
feat(AcControl): add air conditioner component
Browse files Browse the repository at this point in the history
  • Loading branch information
rtrompier committed Aug 13, 2023
1 parent 0cbbb54 commit b007ca1
Show file tree
Hide file tree
Showing 9 changed files with 303 additions and 31 deletions.
91 changes: 91 additions & 0 deletions spec/air-conditionner.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Axios } from 'axios-observable';
import nock from 'nock';
import { map } from 'rxjs/operators';
import { Server } from '../src/server';

const config = require('./support/config_test.json');
const loxoneDiscoverResponse = require('./responses/loxone-discover.json');
let app;

describe('Air Conditionner', () => {
beforeAll((done: DoneFn) => {
const url = `${config.loxone.protocol}://${config.loxone.url}`;

nock(url)
.post('/data/LoxApp3.json')
.reply(200, loxoneDiscoverResponse);

app = new Server({
jwt: __dirname + '/support/jwt.json',
config: __dirname + '/support/config_test.json',
port: 3000,
}, done);
});

afterAll((done) => {
app?.server?.close(done);
});

it('should request the status of a temperature', (done: DoneFn) => {
Axios.post(`http://localhost:3000/smarthome`, {
requestId: 'ff36a3cc-ec34-11e6-b1a0-64510650abcf',
inputs: [{
intent: 'action.devices.QUERY',
payload: {
devices: [
{
id: '10f4ff00-0155-692f-ffff6322d0f91670'
}
]
}
}]
}, {
headers: {
Authorization: 'Bearer access-token-from-skill',
}
})
.pipe(map((resp: any) => resp.data))
.subscribe((resp) => {
expect(resp.payload.devices['10f4ff00-0155-692f-ffff6322d0f91670'].online).toBeTruthy();
// TODO : Test status
done();
});
});

// TODO : Mock Loxone Socket
// it('should update the target temperature', (done: DoneFn) => {
// Axios.post(`http://localhost:3000/smarthome`, {
// requestId: 'ff36a3cc-ec34-11e6-b1a0-64510650abcf',
// inputs: [{
// intent: 'action.devices.QUERY',
// payload: {
// "commands": [{
// "devices": [{
// "id": "1b70c016-0396-1f6d-ffff0be037a23d47"
// }],
// "execution": [{
// "command": "action.devices.commands.ThermostatTemperatureSetpoint",
// "params": {
// "thermostatTemperatureSetpoint": 24
// }
// }]
// }]
// }
// }]
// }, {
// headers: {
// Authorization: 'Bearer access-token-from-skill',
// }
// })
// .pipe(map((resp: any) => resp.data))
// .subscribe((resp) => {
// expect(resp.payload.devices['10f4ff00-0155-692f-ffff6322d0f91670'].online).toBeTruthy();
// expect(resp.payload.devices['10f4ff00-0155-692f-ffff6322d0f91670'].thermostatTemperatureSetpoint).toBe('24');
// // TODO : Test status
// done();
// });
// });

});


26 changes: 26 additions & 0 deletions spec/responses/loxone-discover.json
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,32 @@
"states": {
"position": "10f4ff00-0155-693c-ffff6322d0f91668"
}
},
"10f4ff00-0155-692f-ffff6322d0f91670": {
"name": "Clim",
"type": "AcControl",
"uuidAction": "10f4ff00-0155-692f-ffff6322d0f91668",
"room": "10f4fbeb-0306-5af6-ffff0be037a23d47",
"cat": "103defc1-02e7-078d-ffff0be037a23d47",
"defaultRating": 10,
"isFavorite": true,
"isSecured": false,
"states": {
"jLocked": "9b70c2cf-020d-94f6-02ffe0e20d696b15",
"status": "1b70c2cf-020d-9501-0dffe0e20d696b15",
"mode": "1b70c2cf-020d-9502-0effe0e20d696b15",
"fan": "1b70c2cf-020d-9503-0fffe0e20d696b15",
"ventMode": "1b70c2cf-020d-9504-10ffe0e20d696b15",
"targetTemperature": "1b70c2cf-020d-9505-11ffe0e20d696b15",
"temperature": "1b70c2cf-020d-9506-12ffe0e20d696b15",
"pauseTime": "1b70c2cf-020d-9500-0cffe0e20d696b15",
"operatingModes": "1b70c2cf-020d-94ee-ffffe0e20d696b15",
"fanspeeds": "1b70c2cf-020d-94f0-ffffe0e20d696b15",
"airflows": "1b70c2cf-020d-94ef-ffffe0e20d696b15",
"override": "1b70c2cf-020d-94f1-ffffe0e20d696b15",
"pauseUntil": "1b70c2cf-020d-94f2-ffffe0e20d696b15",
"pauseReason": "1b70c2cf-020d-94f3-ffffe0e20d696b15"
}
}
}
}
5 changes: 4 additions & 1 deletion spec/sync.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ describe('Sync', () => {
})
.pipe(map((resp: any) => resp.data))
.subscribe((resp) => {
expect(resp.payload.devices.length).toEqual(5);
expect(resp.payload.devices.length).toEqual(6);

expect(resp.payload.devices[0].id).toEqual('10f5096d-0338-13c2-ffffd75a488e408a');
expect(resp.payload.devices[0].type).toEqual('action.devices.types.LIGHT');
Expand All @@ -56,6 +56,9 @@ describe('Sync', () => {
expect(resp.payload.devices[4].id).toEqual('10f4ff00-0155-692f-ffff6322d0f91669');
expect(resp.payload.devices[4].type).toEqual('action.devices.types.LIGHT');

expect(resp.payload.devices[5].id).toEqual('10f4ff00-0155-692f-ffff6322d0f91670');
expect(resp.payload.devices[5].type).toEqual('action.devices.types.AIRCOOLER');

done();
});
});
Expand Down
2 changes: 2 additions & 0 deletions src/capabilities/capability-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { EndpointHealthHandler } from './endpoint-health';
import { OnOffHandler } from './on-off';
import { OpenCloseHandler } from './open-close';
import { TemperatureControlHandler } from './temperature-control';
import { TemperatureSettingHandler } from './temperature-setting';

/* tslint:disable no-empty-interface */
export interface Capability {
Expand Down Expand Up @@ -31,6 +32,7 @@ export class Handlers {
BrightnessHandler.INSTANCE,
EndpointHealthHandler.INSTANCE,
TemperatureControlHandler.INSTANCE,
TemperatureSettingHandler.INSTANCE,
OpenCloseHandler.INSTANCE
];

Expand Down
21 changes: 17 additions & 4 deletions src/capabilities/temperature-setting.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Observable, of } from 'rxjs';
import { Observable, map, of } from 'rxjs';
import { Capability, CapabilityHandler } from './capability-handler';
import { TemperatureControl } from './temperature-control';

Expand All @@ -12,6 +12,10 @@ export class TemperatureState {

export interface TemperatureSetting extends Capability {
getTemperature(): Observable<TemperatureState>;

setTemperature(temp: number): Observable<boolean>;

setMode(mode: string): Observable<boolean>;
}

export class TemperatureSettingHandler implements CapabilityHandler<TemperatureSetting> {
Expand All @@ -20,6 +24,7 @@ export class TemperatureSettingHandler implements CapabilityHandler<TemperatureS
getCommands(): string[] {
return [
'action.devices.commands.ThermostatTemperatureSetpoint',
'action.devices.commands.ThermostatSetMode',
'action.devices.commands.ThermostatTemperatureSetRange',
'action.devices.commands.ThermostatSetMode'
];
Expand All @@ -35,13 +40,21 @@ export class TemperatureSettingHandler implements CapabilityHandler<TemperatureS

getAttributes(component: TemperatureControl): any {
return {
'availableThermostatModes': 'off,on,heat,cool,heatcool',
'availableThermostatModes': 'off,on,heat,cool,heatcool,auto,dry,fan-only',
'thermostatTemperatureUnit': 'C'
}
}

handleCommands(component: TemperatureSetting, command: string, payload?: any): Observable<boolean> {
console.log('No TemperatureComponent control handle');
return of(true);
switch (command) {
case 'action.devices.commands.ThermostatTemperatureSetpoint':
return component.setTemperature(payload.thermostatTemperatureSetpoint)
.pipe(map(() => true));
case 'action.devices.commands.ThermostatSetMode':
return component.setMode(payload.thermostatMode)
.pipe(map(() => true));
default:
return of(true);
}
}
}
119 changes: 119 additions & 0 deletions src/components/air-cooler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { map, Observable, of, Subject } from 'rxjs';
import { CapabilityHandler } from '../capabilities/capability-handler';
import { TemperatureSetting, TemperatureSettingHandler, TemperatureState } from '../capabilities/temperature-setting';
import { ComponentRaw } from '../config';
import { LoxoneRequest } from '../loxone-request';
import { Component } from './component';
import { OnOff, OnOffHandler } from '../capabilities/on-off';
import { ErrorType } from '../error';

export class AirCoolerComponent extends Component implements OnOff, TemperatureSetting {
private on: boolean;
protected temperatureState: TemperatureState = new TemperatureState();

constructor(rawComponent: ComponentRaw, loxoneRequest: LoxoneRequest, statesEvents: Subject<Component>) {
super(rawComponent, loxoneRequest, statesEvents);

this.loxoneRequest.getControlInformation(this.loxoneId).subscribe(temperature => {
this.loxoneRequest.watchComponent(temperature.states.temperature).subscribe((event) => {
if(event === 0) {
console.log('Ambient temperature not managed by the airconditionner, try to find a a sensor in the same room.');
// TODO : unsubscribe the current event
// TODO : find in the same room a temperature sensor
}

this.temperatureState.thermostatTemperatureAmbient = parseInt(event); // Current temp is not returned by Loxone
this.statesEvents.next(this);
});
this.loxoneRequest.watchComponent(temperature.states.status).subscribe((event) => {
switch (parseInt(event)) {
case 0: this.on = false; break;
case 1: this.on = true; break;
}
this.statesEvents.next(this);
});
this.loxoneRequest.watchComponent(temperature.states.targetTemperature).subscribe((event) => {
this.temperatureState.thermostatTemperatureSetpoint = parseInt(event, 10);
this.statesEvents.next(this);
});
this.loxoneRequest.watchComponent(temperature.states.mode).subscribe((event) => {
switch (parseInt(event)) {
case 1: this.temperatureState.thermostatMode = 'auto'; break;
case 2: this.temperatureState.thermostatMode = 'heat'; break;
case 3: this.temperatureState.thermostatMode = 'cool'; break;
case 4: this.temperatureState.thermostatMode = 'dry'; break;
case 5: this.temperatureState.thermostatMode = 'fan-only'; break;
}
this.statesEvents.next(this);
});
});
}

turnOn(): Observable<boolean> {
return this.loxoneRequest.sendCmd(this.loxoneId, 'on').pipe(map((result) => {
if (result.code === '200') {
this.on = true;
this.statesEvents.next(this);
return true;
}
throw new Error(ErrorType.ENDPOINT_UNREACHABLE)
}))
}

turnOff(): Observable<boolean> {
return this.loxoneRequest.sendCmd(this.loxoneId, 'off').pipe(map((result) => {
if (result.code === '200') {
this.on = false;
this.statesEvents.next(this);
return true;
}
throw new Error(ErrorType.ENDPOINT_UNREACHABLE);
}));
}

getPowerState(): Observable<boolean> {
return of(this.on);
}

getCapabilities(): CapabilityHandler<any>[] {
return [
OnOffHandler.INSTANCE,
TemperatureSettingHandler.INSTANCE
];
}

getTemperature(): Observable<TemperatureState> {
return of(this.temperatureState)
}

setTemperature(temp: number): Observable<boolean> {
return this.loxoneRequest.sendCmd(this.loxoneId, `setTarget/${temp}`).pipe(map((result) => {
if (result.code === '200') {
this.temperatureState.thermostatTemperatureSetpoint = temp;
this.statesEvents.next(this);
return true;
}
throw new Error(ErrorType.ENDPOINT_UNREACHABLE);
}));
}

setMode(mode: string): Observable<boolean> {
let loxoneMode: number;
switch (mode) {
case 'auto': loxoneMode = 1; break;
case 'heat': loxoneMode = 2; break;
case 'cool': loxoneMode = 3; break;
case 'dry': loxoneMode = 4; break;
case 'fan-only': loxoneMode = 5; break;
}

return this.loxoneRequest.sendCmd(this.loxoneId, `setMode/${loxoneMode}`).pipe(map((result) => {
if (result.code === '200') {
this.temperatureState.thermostatMode = mode;
this.statesEvents.next(this);
return true;
}
throw new Error(ErrorType.ENDPOINT_UNREACHABLE);
}));
}
}
8 changes: 7 additions & 1 deletion src/components/components.factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Component } from './component';
import { LightComponent } from './light';
import { SwitchComponent } from './switch';
import { TemperatureComponent } from './temperature';
import { AirCoolerComponent } from './air-cooler';

export class ComponentsFactory {
private components: { [key: string]: Component } = {};
Expand Down Expand Up @@ -55,6 +56,9 @@ export class ComponentsFactory {
case 'IRoomControllerV2':
component = new TemperatureComponent(rawComponent, this.loxoneRequest, this.statesEvents);
break;
case 'AcControl':
component = new AirCoolerComponent(rawComponent, this.loxoneRequest, this.statesEvents);
break;
// case 'WindowMonitor':
// component = new OpenCloseSensorComponent(rawComponent, this.loxoneRequest, this.statesEvents);
// break;
Expand All @@ -80,7 +84,7 @@ export class ComponentsFactory {
return this.components;
}

private extractType(loxoneType: string): 'LIGHT' | 'THERMOSTAT' | 'BLINDS' | 'SWITCH' | 'SENSOR' {
private extractType(loxoneType: string): 'LIGHT' | 'THERMOSTAT' | 'BLINDS' | 'SWITCH' | 'SENSOR' | 'AIRCOOLER' {
switch (loxoneType) {
case 'Switch':
return 'LIGHT';
Expand All @@ -94,6 +98,8 @@ export class ComponentsFactory {
return 'THERMOSTAT';
case 'WindowMonitor':
return 'SENSOR';
case 'AcControl':
return 'AIRCOOLER';
}
}
}
Loading

0 comments on commit b007ca1

Please sign in to comment.