Skip to content

Commit

Permalink
Merge pull request #27 from justjam2013/window-covering-accessory
Browse files Browse the repository at this point in the history
Add Window Covering accessory
  • Loading branch information
justjam2013 authored Nov 23, 2024
2 parents 1f8ee30 + 22a510c commit 726f9db
Show file tree
Hide file tree
Showing 7 changed files with 285 additions and 31 deletions.
65 changes: 39 additions & 26 deletions config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,30 @@
{ "title": "Garage Door", "enum": ["garagedoor"] },
{ "title": "Lock", "enum": ["lock"] },
{ "title": "Sensor", "enum": ["sensor"] },
{ "title": "Switch", "enum": ["switch"] }
{ "title": "Switch", "enum": ["switch"] },
{ "title": "Window Covering - Blinds, Shades", "enum": ["windowcovering"] }
],
"default": "switch"
},
"switchDefaultState": {
"title": "Switch Default State *",
"description": "Switch default state",
"doorbellVolume": {
"title": "Doorbell Volume *",
"description": "Doorbell volume *",
"type": "integer",
"minimum": 0,
"condition": {
"functionBody": "return model.devices[arrayIndices].accessoryType === 'doorbell';"
}
},
"garageDoorDefaultState": {
"title": "Garage Door Default State *",
"description": "Garage Door default state",
"type": "string",
"oneOf": [
{ "title": "Off", "enum": ["off"] },
{ "title": "On", "enum": ["on"] }
{ "title": "Closed", "enum": ["closed"] },
{ "title": "Open", "enum": ["open"] }
],
"condition": {
"functionBody": "return model.devices[arrayIndices].accessoryType === 'switch';"
"functionBody": "return model.devices[arrayIndices].accessoryType === 'garagedoor';"
}
},
"lockDefaultState": {
Expand All @@ -74,18 +84,6 @@
"functionBody": "return model.devices[arrayIndices].accessoryType === 'lock';"
}
},
"garageDoorDefaultState": {
"title": "Garage Door Default State *",
"description": "Garage Door default state",
"type": "string",
"oneOf": [
{ "title": "Closed", "enum": ["closed"] },
{ "title": "Open", "enum": ["open"] }
],
"condition": {
"functionBody": "return model.devices[arrayIndices].accessoryType === 'garagedoor';"
}
},
"lockHardwareFinish": {
"title": "Lock Hardware Finish *",
"description": "Color of the virtual HomeKey card in the Wallet app",
Expand All @@ -100,13 +98,28 @@
"functionBody": "return model.devices[arrayIndices].accessoryType === 'lock';"
}
},
"doorbellVolume": {
"title": "Doorbell Volume *",
"description": "Doorbell volume *",
"type": "integer",
"minimum": 0,
"switchDefaultState": {
"title": "Switch Default State *",
"description": "Switch default state",
"type": "string",
"oneOf": [
{ "title": "Off", "enum": ["off"] },
{ "title": "On", "enum": ["on"] }
],
"condition": {
"functionBody": "return model.devices[arrayIndices].accessoryType === 'doorbell';"
"functionBody": "return model.devices[arrayIndices].accessoryType === 'switch';"
}
},
"windowCoveringDefaultState": {
"title": "Window Covering Default State *",
"description": "Window Covering default state",
"type": "string",
"oneOf": [
{ "title": "Closed", "enum": ["closed"] },
{ "title": "Open", "enum": ["open"] }
],
"condition": {
"functionBody": "return model.devices[arrayIndices].accessoryType === 'windowcovering';"
}
},
"sensorType": {
Expand Down Expand Up @@ -143,7 +156,7 @@
"description": "Accessory state survives Homebridge restart",
"type": "boolean",
"condition": {
"functionBody": "return ['switch', 'lock', 'garagedoor'].includes(model.devices[arrayIndices].accessoryType) && [undefined, false].includes(model.devices[arrayIndices].accessoryHasResetTimer);"
"functionBody": "return ['switch', 'lock', 'garagedoor', 'windowcovering'].includes(model.devices[arrayIndices].accessoryType) && [undefined, false].includes(model.devices[arrayIndices].accessoryHasResetTimer);"
}
},
"accessoryHasResetTimer": {
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "homebridge-virtual-accessories",
"displayName": "Virtual Accessories for Homebridge",
"type": "module",
"version": "1.0.5",
"version": "1.1.0-beta.2",
"description": "Virtual accessories for Homebridge.",
"author": "justjam2013",
"license": "Apache-2.0",
Expand Down
4 changes: 2 additions & 2 deletions src/accessories/virtualAccessoryGarageDoor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export class GarageDoor extends Accessory {
// implement your own code to check if the device is on
const garageDoorState = this.states.GarageDoorState;

this.platform.log.debug(`[${this.accessoryConfiguration.accessoryName}] Getting Current Door State: ${this.getStateName(garageDoorState)}`);
this.platform.log.debug(`[${this.accessoryConfiguration.accessoryName}] Getting Current Garage Door State: ${this.getStateName(garageDoorState)}`);

// if you need to return an error to show the device as "Not Responding" in the Home app:
// throw new this.platform.api.hap.HapStatusError(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
Expand Down Expand Up @@ -158,7 +158,7 @@ export class GarageDoor extends Accessory {
// implement your own code to check if the device is on
const garageDoorState = this.states.GarageDoorState;

this.platform.log.debug(`[${this.accessoryConfiguration.accessoryName}] Getting Target Door State: ${this.getStateName(garageDoorState)}`);
this.platform.log.debug(`[${this.accessoryConfiguration.accessoryName}] Getting Target Garage Door State: ${this.getStateName(garageDoorState)}`);

// if you need to return an error to show the device as "Not Responding" in the Home app:
// throw new this.platform.api.hap.HapStatusError(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
Expand Down
221 changes: 221 additions & 0 deletions src/accessories/virtualAccessoryWindowCovering.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
/* eslint-disable max-len */

import type { CharacteristicValue, PlatformAccessory } from 'homebridge';

import { VirtualAccessoryPlatform } from '../platform.js';
import { Accessory } from './virtualAccessory.js';

/**
* WindowCovering - Accessory implementation
*/
export class WindowCovering extends Accessory {

static readonly CLOSED: number = 0; // 0%
static readonly OPEN: number = 100; // 100%

static readonly DECREASING: number = 0; // Characteristic.PositionState.DECREASING; -> CLOSING
static readonly INCREASING: number = 1; // Characteristic.PositionState.INCREASING; -> OPENING
static readonly STOPPED: number = 2; // Characteristic.PositionState.STOPPED; -> OPEN or CLOSED

/**
* These are just used to create a working example
* You should implement your own code to track the state of your accessory
*/
private states = {
WindowCoveringCurrentPosition: WindowCovering.CLOSED,
// WindowCoveringTargetPosition: WindowCovering.CLOSED,
WindowCoveringPositionState: WindowCovering.STOPPED,
};

private readonly stateStorageKey: string = 'WindowCoveringPosition';

constructor(
platform: VirtualAccessoryPlatform,
accessory: PlatformAccessory,
) {
super(platform, accessory);

// First configure the device based on the accessory details
this.defaultState = this.accessoryConfiguration.windowCoveringDefaultState === 'open' ? WindowCovering.OPEN : WindowCovering.CLOSED;

// If the accessory is stateful retrieve stored state, otherwise set to default state
if (this.accessoryConfiguration.accessoryIsStateful) {
const cachedState = this.loadState(this.storagePath, this.stateStorageKey) as number;

if (cachedState !== undefined) {
this.states.WindowCoveringCurrentPosition = cachedState;
} else {
this.states.WindowCoveringCurrentPosition = this.defaultState;
}
} else {
this.states.WindowCoveringCurrentPosition = this.defaultState;
}

// set accessory information
this.accessory.getService(this.platform.Service.AccessoryInformation)!
.setCharacteristic(this.platform.Characteristic.Manufacturer, 'Virtual Accessories for Homebridge')
.setCharacteristic(this.platform.Characteristic.Model, 'Virtual Accessory - Window Covering')
.setCharacteristic(this.platform.Characteristic.SerialNumber, this.accessory.UUID);

// get the LightBulb service if it exists, otherwise create a new LightBulb service
// you can create multiple services for each accessory
this.service = this.accessory.getService(this.platform.Service.WindowCovering) || this.accessory.addService(this.platform.Service.WindowCovering);

// set the service name, this is what is displayed as the default name on the Home app
// in this example we are using the name we stored in the `accessory.context` in the `discoverDevices` method.
this.service.setCharacteristic(this.platform.Characteristic.Name, this.accessoryConfiguration.accessoryName);

// Update the initial state of the accessory
this.platform.log.debug(`[${this.accessoryConfiguration.accessoryName}] Setting Window Covering Current Position: ${this.getStateName(this.states.WindowCoveringCurrentPosition)}`);
this.service.updateCharacteristic(this.platform.Characteristic.CurrentPosition, (this.states.WindowCoveringCurrentPosition));
this.service.updateCharacteristic(this.platform.Characteristic.TargetPosition, (this.states.WindowCoveringCurrentPosition));
this.service.updateCharacteristic(this.platform.Characteristic.PositionState, (this.states.WindowCoveringPositionState));

// each service must implement at-minimum the "required characteristics" for the given service type
// see https://developers.homebridge.io/#/service/Lightbulb

// register handlers for the CurrentDoorState Characteristic
this.service.getCharacteristic(this.platform.Characteristic.CurrentPosition)
.onGet(this.handleCurrentPositionGet.bind(this)); // GET - bind to the 'handleCurrentPositionGet` method below

// register handlers for the TargetDoorState Characteristic
this.service.getCharacteristic(this.platform.Characteristic.TargetPosition)
.onSet(this.handleTargetPositionSet.bind(this)) // SET - bind to the `handleTargetPositionSet` method below
.onGet(this.handleTargetPositionGet.bind(this)); // GET - bind to the `handleTargetPositionGet` method below

// register handlers for the ObstructionDetected Characteristic
this.service.getCharacteristic(this.platform.Characteristic.PositionState)
.onGet(this.handlePositionStateGet.bind(this)); // GET - bind to the 'handlePositionStateGet` method below

/**
* Creating multiple services of the same type.
*
* To avoid "Cannot add a Service with the same UUID another Service without also defining a unique 'subtype' property." error,
* when creating multiple services of the same type, you need to use the following syntax to specify a name and subtype id:
* this.accessory.getService('NAME') || this.accessory.addService(this.platform.Service.Lightbulb, 'NAME', 'USER_DEFINED_SUBTYPE_ID');
*
* The USER_DEFINED_SUBTYPE must be unique to the platform accessory (if you platform exposes multiple accessories, each accessory
* can use the same subtype id.)
*/

}

/**
* Handle "GET" requests from HomeKit
*/
async handleCurrentPositionGet() {
// implement your own code to check if the device is on
const windowCoveringCurrentPosition = this.states.WindowCoveringCurrentPosition;

this.platform.log.debug(`[${this.accessoryConfiguration.accessoryName}] Getting Current Window Covering Position: ${this.getStateName(windowCoveringCurrentPosition)}`);

// if you need to return an error to show the device as "Not Responding" in the Home app:
// throw new this.platform.api.hap.HapStatusError(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);

return windowCoveringCurrentPosition;
}

/**
* Handle "SET" requests from HomeKit
* These are sent when the user changes the state of an accessory, for example, turning on a Light bulb.
*/
async handleTargetPositionSet(value: CharacteristicValue) {
// implement your own code to turn your device on/off
this.states.WindowCoveringCurrentPosition = value as number;

// Store device state if stateful
if (this.accessoryConfiguration.accessoryIsStateful) {
this.saveState(this.storagePath, this.stateStorageKey, this.states.WindowCoveringCurrentPosition);
}

this.platform.log.info(`[${this.accessoryConfiguration.accessoryName}] Setting Target Window Covering Position: ${this.getStateName(this.states.WindowCoveringCurrentPosition)}`);

// PositionState DECREASING/INCREASING
this.states.WindowCoveringPositionState = (this.states.WindowCoveringCurrentPosition === WindowCovering.OPEN) ? WindowCovering.INCREASING : WindowCovering.DECREASING;
this.service!.setCharacteristic(this.platform.Characteristic.PositionState, (this.states.WindowCoveringPositionState));
this.platform.log.info(`[${this.accessoryConfiguration.accessoryName}] Setting Curent Window Covering State: ${this.getPositionName(this.states.WindowCoveringPositionState)}`);

// PositionState STOPPED
// CurrentPosition OPEN/CLOSED with 3 second delay
const transitionDelayMillis: number = 3 * 1000;
setTimeout(() => {
this.states.WindowCoveringPositionState = WindowCovering.STOPPED;
this.service!.setCharacteristic(this.platform.Characteristic.PositionState, (this.states.WindowCoveringPositionState));
this.platform.log.info(`[${this.accessoryConfiguration.accessoryName}] Setting Curent Window Covering State: ${this.getPositionName(this.states.WindowCoveringPositionState)}`);

this.service!.setCharacteristic(this.platform.Characteristic.CurrentPosition, (this.states.WindowCoveringCurrentPosition));
this.platform.log.info(`[${this.accessoryConfiguration.accessoryName}] Setting Current Garage Door State: ${this.getStateName(this.states.WindowCoveringCurrentPosition)}`);
}, transitionDelayMillis);
}

/**
* Handle the "GET" requests from HomeKit
* These are sent when HomeKit wants to know the current state of the accessory, for example, checking if a Light bulb is on.
*
* GET requests should return as fast as possible. A long delay here will result in
* HomeKit being unresponsive and a bad user experience in general.
*
* If your device takes time to respond you should update the status of your device
* asynchronously instead using the `updateCharacteristic` method instead.
* @example
* this.service.updateCharacteristic(this.platform.Characteristic.On, true)
*/
async handleTargetPositionGet(): Promise<CharacteristicValue> {
// implement your own code to check if the device is on
const windowCoveringPosition = this.states.WindowCoveringCurrentPosition;

this.platform.log.debug(`[${this.accessoryConfiguration.accessoryName}] Getting Target Window Covering Position: ${this.getStateName(windowCoveringPosition)}`);

// if you need to return an error to show the device as "Not Responding" in the Home app:
// throw new this.platform.api.hap.HapStatusError(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);

return windowCoveringPosition;
}

/**
* Handle "GET" requests from HomeKit
*/
async handlePositionStateGet() {
// implement your own code to check if the device is on
const windowCoveringPosition = this.states.WindowCoveringPositionState;

this.platform.log.debug(`[${this.accessoryConfiguration.accessoryName}] Getting Window Covering State: ${this.getPositionName(windowCoveringPosition)}`);

// if you need to return an error to show the device as "Not Responding" in the Home app:
// throw new this.platform.api.hap.HapStatusError(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);

return windowCoveringPosition;
}

private getStateName(position: number): string {
let positionName: string;

switch (position) {
case undefined: { positionName = 'undefined'; break; }
case WindowCovering.CLOSED: { positionName = 'CLOSED'; break; }
case WindowCovering.OPEN: { positionName = 'OPEN'; break; }
default: { positionName = `POSITION: ${position.toString()}%`; }
}

if (position > WindowCovering.OPEN) {
positionName = `INVALID ${positionName}%`;
}

return positionName;
}

private getPositionName(state: number): string {
let stateName: string;

switch (state) {
case undefined: { stateName = 'undefined'; break; }
case WindowCovering.DECREASING: { stateName = 'DECREASING'; break; }
case WindowCovering.INCREASING: { stateName = 'INCREASING'; break; }
case WindowCovering.STOPPED: { stateName = 'STOPPED'; break; }
default: { stateName = state.toString(); }
}

return stateName;
}
}
4 changes: 4 additions & 0 deletions src/accessoryFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Switch } from './accessories/virtualAccessorySwitch.js';
import { Lock } from './accessories/virtualAccessoryLock.js';
import { Doorbell } from './accessories/virtualAccessoryDoorbell.js';
import { GarageDoor } from './accessories/virtualAccessoryGarageDoor.js';
import { WindowCovering } from './accessories/virtualAccessoryWindowCovering.js';

import { VirtualSensor } from './sensors/virtualSensor.js';
import { VirtualContactSensor } from './sensors/virtualSensorContact.js';
Expand Down Expand Up @@ -55,6 +56,9 @@ export abstract class AccessoryFactory {
case 'garagedoor':
virtualAccessory = new GarageDoor(platform, accessory);
break;
case 'windowcovering':
virtualAccessory = new WindowCovering(platform, accessory);
break;
case 'sensor':
virtualAccessory = AccessoryFactory.createVirtualSensor(platform, accessory, accessoryConfiguration.sensorType);
break;
Expand Down
Loading

0 comments on commit 726f9db

Please sign in to comment.