Skip to content

Commit

Permalink
feat(shell): add option to configure switches
Browse files Browse the repository at this point in the history
  • Loading branch information
mKeRix committed Apr 5, 2020
1 parent 17bc793 commit 51b985f
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 5 deletions.
4 changes: 4 additions & 0 deletions config/test.yml
Expand Up @@ -21,3 +21,7 @@ shell:
deviceClass: 'timestamp'
icon: 'mdi:test'
unitOfMeasurement: 'tests'
switches:
- name: Test Switch
onCommand: echo on
offCommand: echo off
20 changes: 17 additions & 3 deletions docs/integrations/shell.md
Expand Up @@ -8,9 +8,10 @@ The commands are run with the same user that room-assistant is running with, so

## Settings

| Name | Type | Default | Description |
| --------- | ------------------------- | ------- | ------------------------------------- |
| `sensors` | [Shell Sensors](#sensors) | | An array of shell sensor definitions. |
| Name | Type | Default | Description |
| ---------- | --------------------------- | ------- | ------------------------------------- |
| `sensors` | [Shell Sensors](#sensors) | | An array of shell sensor definitions. |
| `switches` | [Shell Switches](#switches) | | An array of shell switch definitions. |

### Sensors

Expand All @@ -24,6 +25,15 @@ The commands are run with the same user that room-assistant is running with, so
| `unitOfMeasurement` | String | | Unit of measurement of the sensor state, leave empty if none. |
| `deviceClass` | String | | Home Assistant [device class](https://www.home-assistant.io/integrations/sensor/#device-class) to be used for this sensor. |

### Switches

| Name | Type | Default | Description |
| ------------ | ------ | ------- | ------------------------------------------------------------ |
| `name` | String | | Friendly name of this switch. |
| `onCommand` | String | | Shell command that should be executed when the switch is turned on. |
| `offCommand` | String | | Shell command that should be executed when the switch is turned off. |
| `icon` | String | | Icon that the sensor should be represented with in Home Assistant. |

::: details Example Config

```yaml
Expand All @@ -39,6 +49,10 @@ shell:
icon: mdi:wifi
unitOfMeasurement: dBm
deviceClass: signal_strength
switches:
- name: Onboard LED
onCommand: 'echo mmc0 | sudo tee /sys/class/leds/led0/trigger'
offCommand: 'echo none | sudo tee /sys/class/leds/led0/trigger && echo 1 | sudo tee /sys/class/leds/led0/brightness'
```

:::
8 changes: 8 additions & 0 deletions src/integrations/shell/shell.config.ts
@@ -1,5 +1,6 @@
export class ShellConfig {
sensors: ShellSensorOptions[] = [];
switches: ShellSwitchOptions[] = [];
}

class ShellSensorOptions {
Expand All @@ -11,3 +12,10 @@ class ShellSensorOptions {
unitOfMeasurement?: string;
deviceClass?: string;
}

class ShellSwitchOptions {
name: string;
onCommand: string;
offCommand: string;
icon?: string;
}
14 changes: 12 additions & 2 deletions src/integrations/shell/shell.service.spec.ts
Expand Up @@ -9,6 +9,7 @@ import { ScheduleModule, SchedulerRegistry } from '@nestjs/schedule';
import { SensorConfig } from '../home-assistant/sensor-config';
import { CronJob } from 'cron';
import * as util from 'util';
import { ShellSwitch } from './shell.switch';

jest.mock('cron');
jest.mock('util', () => ({
Expand Down Expand Up @@ -43,10 +44,10 @@ describe('ShellService', () => {
service = module.get<ShellService>(ShellService);
});

it('should register sensors on bootstrap', () => {
it('should register entities on bootstrap', () => {
service.onApplicationBootstrap();

expect(entitiesService.add).toHaveBeenCalledTimes(2);
expect(entitiesService.add).toHaveBeenCalledTimes(3);
expect(entitiesService.add).toHaveBeenCalledWith(
new Sensor('shell-simple-test', 'Simple Test'),
expect.any(Array)
Expand All @@ -55,6 +56,15 @@ describe('ShellService', () => {
new Sensor('shell-regex-test', 'Regex Test'),
expect.any(Array)
);
expect(entitiesService.add).toHaveBeenCalledWith(
new ShellSwitch(
'shell-test-switch',
'Test Switch',
'echo on',
'echo off'
),
expect.any(Array)
);
});

it('should pass on entity customizations', () => {
Expand Down
42 changes: 42 additions & 0 deletions src/integrations/shell/shell.service.ts
Expand Up @@ -10,6 +10,9 @@ import { Sensor } from '../../entities/sensor';
import { EntityCustomization } from '../../entities/entity-customization.interface';
import { SensorConfig } from '../home-assistant/sensor-config';
import { CronJob } from 'cron';
import { Switch } from '../../entities/switch';
import { SwitchConfig } from '../home-assistant/switch-config';
import { ShellSwitch } from './shell.switch';

@Injectable()
export class ShellService implements OnApplicationBootstrap {
Expand Down Expand Up @@ -49,6 +52,15 @@ export class ShellService implements OnApplicationBootstrap {
);
job.start();
});

this.config.switches.forEach(switchOptions => {
this.createSwitch(
switchOptions.name,
switchOptions.onCommand,
switchOptions.offCommand,
switchOptions.icon
);
});
}

/**
Expand Down Expand Up @@ -105,4 +117,34 @@ export class ShellService implements OnApplicationBootstrap {
customizations
) as Sensor;
}

/**
* Creates a shell switch.
*
* @param name - Name of the switch
* @param onCommand - Shell command to execute when turned on
* @param offCommand - Shell command to execute when turned off
* @param icon - Icon to use
* @returns Registered switch
*/
protected createSwitch(
name: string,
onCommand: string,
offCommand: string,
icon?: string
): Switch {
const id = makeId(`shell ${name}`);
const customizations: Array<EntityCustomization<any>> = [
{
for: SwitchConfig,
overrides: {
icon
}
}
];
return this.entitiesService.add(
new ShellSwitch(id, name, onCommand, offCommand),
customizations
) as Switch;
}
}
43 changes: 43 additions & 0 deletions src/integrations/shell/shell.switch.spec.ts
@@ -0,0 +1,43 @@
const mockExec = jest.fn();

import { ShellSwitch } from './shell.switch';

jest.mock('util', () => ({
...jest.requireActual('util'),
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
promisify: () => mockExec
}));

describe('ShellSwitch', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('should execute the onCommand when turned on', async () => {
const onCommand = 'echo on';
const shellSwitch = new ShellSwitch('test', 'Test', onCommand, 'echo off');
await shellSwitch.turnOn();

expect(mockExec).toHaveBeenCalledWith(onCommand);
expect(shellSwitch.state).toBeTruthy();
});

it('should execute the offCommand when turned off', async () => {
const offCommand = 'echo off';
const shellSwitch = new ShellSwitch('test', 'Test', 'echo on', offCommand);
await shellSwitch.turnOff();

expect(mockExec).toHaveBeenCalledWith(offCommand);
expect(shellSwitch.state).toBeFalsy();
});

it('should not switch the state if command execution fails', async () => {
mockExec.mockRejectedValue(new Error('command failed'));

const shellSwitch = new ShellSwitch('test', 'Test', 'echo on', 'echo off');
shellSwitch.state = false;
await shellSwitch.turnOn();

expect(shellSwitch.state).toBeFalsy();
});
});
43 changes: 43 additions & 0 deletions src/integrations/shell/shell.switch.ts
@@ -0,0 +1,43 @@
import { Switch } from '../../entities/switch';
import * as util from 'util';
import { exec } from 'child_process';
import { Logger } from '@nestjs/common';

const execPromise = util.promisify(exec);

export class ShellSwitch extends Switch {
onCommand: string;
offCommand: string;

constructor(id: string, name: string, onCommand: string, offCommand: string) {
super(id, name, false);
this.onCommand = onCommand;
this.offCommand = offCommand;
}

async turnOn(): Promise<void> {
try {
await execPromise(this.onCommand);
super.turnOn();
} catch (e) {
Logger.error(
`Turning ${this.id} on failed: ${e.message}`,
e.stack,
ShellSwitch.name
);
}
}

async turnOff(): Promise<void> {
try {
await execPromise(this.offCommand);
super.turnOff();
} catch (e) {
Logger.error(
`Turning ${this.id} off failed: ${e.message}`,
e.stack,
ShellSwitch.name
);
}
}
}

0 comments on commit 51b985f

Please sign in to comment.