Skip to content

Commit

Permalink
feat: add support for fans and air fresheners (#91)
Browse files Browse the repository at this point in the history
  • Loading branch information
joeyhage authored Jan 1, 2024
1 parent 0a2b2e3 commit c1ef468
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 3 deletions.
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

See the [roadmap](https://homebridge-alexa-smarthome.canny.io/) for up-to-date, unreleased work in progress.

## [2.0.8] - 2024-01-01

### Added

- Simple fan support (on/off only)
- Simple air freshener support (on/off only)
- Increased debug logging for device capabilities on plugin startup to help with troubleshooting and supporting new device types.

## [2.0.7] - 2024-01-01

### Fixed
Expand Down Expand Up @@ -155,7 +163,8 @@ See the [roadmap](https://homebridge-alexa-smarthome.canny.io/) for up-to-date,

- Support for outlets i.e. smart plugs.

[unreleased]: https://github.com/joeyhage/homebridge-spotify-speaker/compare/v2.0.7...HEAD
[unreleased]: https://github.com/joeyhage/homebridge-spotify-speaker/compare/v2.0.8...HEAD
[2.0.8]: https://github.com/joeyhage/homebridge-spotify-speaker/compare/v2.0.7...v2.0.8
[2.0.7]: https://github.com/joeyhage/homebridge-spotify-speaker/compare/v2.0.6...v2.0.7
[2.0.6]: https://github.com/joeyhage/homebridge-spotify-speaker/compare/v2.0.5...v2.0.6
[2.0.5]: https://github.com/joeyhage/homebridge-spotify-speaker/compare/v2.0.4...v2.0.5
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,13 @@ Node.js versions supported: 16.x, 18.x, 20.x
- Lightbulbs
- Switches
- HomeKit switches do not support brightness so any switches you have that support brightness will appear in HomeKit as Lightbulbs.
- The following Alexa devices appear as Switches in HomeKit and only support On and Off currently:
- Air fresheners
- Vacuum cleaners
- Fans
- Outlets + smart plugs
- Thermostats
- Locks
- Vacuum cleaners
- Air Quality Monitors
- Only confirmed to work with Amazon Air Quality Monitor. Please report an issue if it doesn't work with your air quality monitor.
- Echo smart speakers / Echo smart displays
Expand Down
4 changes: 4 additions & 0 deletions src/accessory/accessory-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import SwitchAccessory from './switch-accessory';
import TelevisionAccessory from './television-accessory';
import TemperatureAccessory from './temperature-accessory';
import ThermostatAccessory from './thermostat-accessory';
import FanAccessory from './fan-accessory';

export default class AccessoryFactory {
static createAccessory(
Expand All @@ -36,6 +37,9 @@ export default class AccessoryFactory {
.with(platform.Service.Switch.UUID, () =>
E.of(new SwitchAccessory(platform, device, platAcc)),
)
.with(platform.Service.Fanv2.UUID, () =>
E.of(new FanAccessory(platform, device, platAcc)),
)
.with(platform.Service.LockMechanism.UUID, () =>
E.of(new LockAccessory(platform, device, platAcc)),
)
Expand Down
71 changes: 71 additions & 0 deletions src/accessory/fan-accessory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import * as A from 'fp-ts/Array';
import * as O from 'fp-ts/Option';
import * as TE from 'fp-ts/TaskEither';
import { flow, identity, pipe } from 'fp-ts/lib/function';
import { CharacteristicValue, Service } from 'homebridge';
import { SupportedActionsType, SupportedNamespacesType } from '../domain/alexa';
import * as mapper from '../mapper/power-mapper';
import BaseAccessory from './base-accessory';
import { FanNamespaces, FanState } from '../domain/alexa/fan';

export default class FanAccessory extends BaseAccessory {
static requiredOperations: SupportedActionsType[] = ['turnOn', 'turnOff'];
service: Service;
namespaces = FanNamespaces;
isExternalAccessory = false;

configureServices() {
this.service =
this.platformAcc.getService(this.Service.Fanv2) ||
this.platformAcc.addService(this.Service.Fanv2, this.device.displayName);

this.service
.getCharacteristic(this.Characteristic.Active)
.onGet(this.handlePowerGet.bind(this))
.onSet(this.handlePowerSet.bind(this));
}

async handlePowerGet(): Promise<boolean> {
const alexaNamespace: SupportedNamespacesType = 'Alexa.PowerController';
const determinePowerState = flow(
O.filterMap<FanState[], FanState>(
A.findFirst(({ namespace }) => namespace === alexaNamespace),
),
O.map(({ value }) => value === 'ON'),
O.tap((s) =>
O.of(this.logWithContext('debug', `Get power result: ${s}`)),
),
);

return pipe(
this.getState(determinePowerState),
TE.match((e) => {
this.logWithContext('errorT', 'Get power', e);
throw this.serviceCommunicationError;
}, identity),
)();
}

async handlePowerSet(value: CharacteristicValue): Promise<void> {
this.logWithContext('debug', `Triggered set power: ${value}`);
if (typeof value !== 'boolean') {
throw this.invalidValueError;
}
const action = mapper.mapHomeKitPowerToAlexaAction(value);
return pipe(
this.platform.alexaApi.setDeviceState(this.device.id, action),
TE.match(
(e) => {
this.logWithContext('errorT', 'Set power', e);
throw this.serviceCommunicationError;
},
() => {
this.updateCacheValue({
value: mapper.mapHomeKitPowerToAlexaValue(value),
namespace: 'Alexa.PowerController',
});
},
),
)();
}
}
11 changes: 11 additions & 0 deletions src/domain/alexa/fan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { CapabilityState, SupportedNamespaces } from './index';

export interface FanState {
namespace: keyof typeof FanNamespaces &
keyof typeof SupportedNamespaces;
value: CapabilityState['value'];
}

export const FanNamespaces = {
'Alexa.PowerController': 'Alexa.PowerController',
} as const;
22 changes: 21 additions & 1 deletion src/mapper/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import { HomebridgeAccessoryInfo } from '../domain/homebridge';
import type { AlexaSmartHomePlatform } from '../platform';
import { generateUuid } from '../util';
import { SupportedActions } from '../domain/alexa';
import FanAccessory from '../accessory/fan-accessory';

const ALEXA_DEVICES_AS_SWITCHES = ['SWITCH', 'AIR_FRESHENER', 'VACUUM_CLEANER'];

export const mapAlexaDeviceToHomeKitAccessoryInfos = (
platform: AlexaSmartHomePlatform,
Expand Down Expand Up @@ -110,7 +113,7 @@ const determineSupportedHomeKitAccessories = (
)
.when(
([type, ops]) =>
(type === 'SWITCH' || type === 'VACUUM_CLEANER') &&
ALEXA_DEVICES_AS_SWITCHES.includes(type) &&
supportsRequiredActions(SwitchAccessory.requiredOperations, ops),
() =>
E.of([
Expand Down Expand Up @@ -142,6 +145,23 @@ const determineSupportedHomeKitAccessories = (
},
]),
)
.when(
([type, ops]) =>
type === 'FAN' &&
supportsRequiredActions(FanAccessory.requiredOperations, ops),
() =>
E.of([
{
altDeviceName: O.none,
deviceType: platform.Service.Fanv2.UUID,
uuid: generateUuid(
platform,
entityId,
device.providerData.deviceType,
),
},
]),
)
.when(
([type, ops]) =>
type === 'SMARTPLUG' &&
Expand Down
7 changes: 7 additions & 0 deletions src/wrapper/alexa-api-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,13 @@ export class AlexaApiWrapper {
}`,
),
),
TE.tapIO((response) =>
this.log.debug(
'BEGIN capabilities for all devices:',
JSON.stringify(response, undefined, 2),
'END capabilities for all devices',
),
),
TE.map(extractRangeCapabilities),
TE.map((rc) => {
this.deviceStore.deviceCapabilities = rc;
Expand Down

0 comments on commit c1ef468

Please sign in to comment.