...
 
Commits (7)
......@@ -40,6 +40,10 @@ publish-npm:
publish-github:
stage: deploy
script:
# Update the package.json and package-lock.json
- "node -e \"fs.writeFileSync('package.json', JSON.stringify((json => {json.name = '@samuelthomas2774/homebridge-airport'; json.publishConfig = {access: 'public'}; return json;})(JSON.parse(fs.readFileSync('package.json', 'utf-8'))), null, 4) + '\\n', 'utf-8')\""
- "node -e \"fs.writeFileSync('package-lock.json', JSON.stringify((json => {json.name = '@samuelthomas2774/homebridge-airport'; return json;})(JSON.parse(fs.readFileSync('package-lock.json', 'utf-8'))), null, 4) + '\\n', 'utf-8')\""
- echo "//npm.pkg.github.com/:_authToken=${GITHUB_NPM_TOKEN}" > .npmrc
- npm --color="always" --registry=https://npm.pkg.github.com/ publish
dependencies:
......
......@@ -18,6 +18,28 @@
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
]
},
{
"type": "node",
"request": "launch",
"name": "Homebridge",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/node_modules/.bin/homebridge",
"env": {
"DEBUG": "*"
},
"args": [
"-D",
"-P",
"${workspaceFolder}",
"-U",
"${workspaceFolder}/data"
],
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
]
}
]
}
{
"yaml.schemas": {
"./config.schema.json": "data/config.yaml"
}
}
homebridge-airport
===
Homebridge plugin for Apple AirPort base stations. Currently only supports monitoring Wi-Fi clients.
```
# From registry.npmjs.com
npm install --global homebridge-airport
# From npm.pkg.github.com
npm install --global --registry https://npm.pkg.github.com @samuelthomas2774/homebridge-airport
```
```json
{
"platform": "airport.AirPort",
"devices": {
"AirPort Extreme": {
"host": "airport-extreme.local",
"password": "password"
}
},
"accessories": [
{
"type": "connected-clients",
"name": "Wi-Fi Network In Use",
"stations": "all",
"exclude-clients": [
"00:00:00:00:00:00"
]
}
]
}
```
#### TODO
- Use a monitoring session instead of polling connected clients
- HomeKit Accessory Security
Configuration
---
### AirPort base station connections
The `devices` object maps names of AirPort base stations to their host/IP address and admin password.
```json
{
"AirPort Extreme": {
"host": "airport-extreme.local",
"password": "password"
}
}
```
You can also add the port number if it's different for any reason (e.g. when using port forwarding to access remote
base stations), and disable the default base station accessory.
```json
{
"AirPort Extreme": {
"host": "airport-extreme.local",
"password": "password",
"port": 5009,
"accessory": false
}
}
```
### Accessories
The `accessories` array contains a list of accessories to publish in Homebridge, with a `type` property and
type-specific configuration.
#### AirPort base station
Accessories for each AirPort base station are automatically added (unless they are explicitly configured or the
default base station accessory is disabled).
`type` must be `"base-station"` and `id` is the key for the base station in the `devices` object.
```json
{
"type": "base-station",
"id": "AirPort Extreme"
}
```
To add an occupancy sensor that detects Wi-Fi clients, set the `connected-clients` property.
```json
{
"type": "base-station",
"id": "AirPort Extreme",
"connected-clients": [
{
"stations": [
"all"
],
"clients": [...],
"exclude-clients": [...]
}
]
}
```
#### Occupancy sensor
Adds an occupancy sensor that detects Wi-Fi clients.
```json
{
"type": "connected-clients",
"name": "Wi-Fi Network In Use",
"stations": [
["AirPort Extreme", "main"]
],
"exclude-clients": [
"00:00:00:00:00:00"
]
}
```
You can also whitelist clients to detect instead of excluding known clients.
```json
{
"type": "connected-clients",
"name": "Wi-Fi Network In Use",
"stations": [
["AirPort Extreme", "main"]
],
"clients": [
"00:00:00:00:00:00"
]
}
```
This diff is collapsed.
This diff is collapsed.
......@@ -16,14 +16,14 @@
"dist"
],
"dependencies": {
"node-acp": "^0.5.0"
"node-acp": "^0.7.0"
},
"devDependencies": {
"@types/node": "^12.12.11",
"@types/node": "^12.12.35",
"@types/yaml": "^1.2.0",
"hap-nodejs": "^0.5.3",
"homebridge": "^0.4.50",
"typescript": "^3.7.2",
"yaml": "^1.7.2"
"hap-nodejs": "^0.5.12-beta.2",
"homebridge": "^0.4.54-beta.23",
"typescript": "^3.8.3",
"yaml": "^1.8.3"
}
}
import Accessory from './accessory';
import AirPortBaseStation, {ClientStatus} from '../airport';
import AirPort from '../platform';
import {Types} from '../custom-hap';
import {CustomHapTypes} from '../custom-hap';
import {BaseStationConfiguration, ConnectedClientsConfiguration} from '../configuration';
import {Property} from 'node-acp';
import {CharacteristicEventTypes} from 'hap-nodejs';
......@@ -25,6 +25,7 @@ export default class BaseStationAccessory extends Accessory {
this.name = identifier;
this.basestation.setStatus(ClientStatus.READY);
this.basestation.auto_reconnect = true;
this.basestation.connect().then(async () => {
const [name, model, serial, version, wifi] =
await this.basestation.client!.getProperties(['syNm', 'syAM', 'sySN', 'syVs', 'WiFi']) as [
......@@ -73,7 +74,7 @@ export default class BaseStationAccessory extends Accessory {
return accessory;
}
readonly wifi_satellite_service: Types['WiFiSatellite'] = (() => {
readonly wifi_satellite_service: CustomHapTypes.WiFiSatellite = (() => {
const service = new this.platform.custom.WiFiSatellite('Wi-Fi Network');
service.getCharacteristic(this.platform.custom.WiFiSatelliteStatus)!
......
......@@ -4,6 +4,7 @@ import AirPort from '../platform';
import {ConnectedClientsConfiguration, MACAddress} from '../configuration';
import {Property, PropertyValueTypes} from 'node-acp';
import {callbackifyGetHandler, readableJoin} from '../util';
import {CharacteristicEventTypes} from 'hap-nodejs';
export default class ConnectedClientsAccessory extends Accessory {
readonly basestations: [AirPortBaseStation, string | null][];
......@@ -32,6 +33,7 @@ export default class ConnectedClientsAccessory extends Accessory {
for (const [basestation] of this.basestations) {
basestation.setStatus(ClientStatus.READY);
basestation.auto_reconnect = true;
}
this.occupancy_sensor_service.setCharacteristic(this.platform.hap.Characteristic.Name, name);
......@@ -68,19 +70,19 @@ export default class ConnectedClientsAccessory extends Accessory {
const service = new this.platform.hap.Service.OccupancySensor('Wi-Fi clients');
service.getCharacteristic(this.platform.custom.WiFiClientList)!
.on('get', this._handleGetClientList.bind(this, false))
.on('subscribe', this._handleSubscribeClientList.bind(this, false))
.on('unsubscribe', this._handleUnsubscribeClientList.bind(this, false));
.on('get' as CharacteristicEventTypes, this._handleGetClientList.bind(this, false))
.on('subscribe' as CharacteristicEventTypes, this._handleSubscribeClientList.bind(this, false))
.on('unsubscribe' as CharacteristicEventTypes, this._handleUnsubscribeClientList.bind(this, false));
service.getCharacteristic(this.platform.custom.FullWiFiClientList)!
.on('get', this._handleGetClientList.bind(this, true))
.on('subscribe', this._handleSubscribeClientList.bind(this, true))
.on('unsubscribe', this._handleUnsubscribeClientList.bind(this, true));
.on('get' as CharacteristicEventTypes, this._handleGetClientList.bind(this, true))
.on('subscribe' as CharacteristicEventTypes, this._handleSubscribeClientList.bind(this, true))
.on('unsubscribe' as CharacteristicEventTypes, this._handleUnsubscribeClientList.bind(this, true));
service.getCharacteristic(this.platform.hap.Characteristic.OccupancyDetected)!
.on('get', this._handleGetOccupancyDetected.bind(this))
.on('subscribe', this._handleSubscribeOccupancyDetected.bind(this))
.on('unsubscribe', this._handleUnsubscribeOccupancyDetected.bind(this));
.on('get' as CharacteristicEventTypes, this._handleGetOccupancyDetected.bind(this))
.on('subscribe' as CharacteristicEventTypes, this._handleSubscribeOccupancyDetected.bind(this))
.on('unsubscribe' as CharacteristicEventTypes, this._handleUnsubscribeOccupancyDetected.bind(this));
return service;
})();
......@@ -100,7 +102,7 @@ export default class ConnectedClientsAccessory extends Accessory {
const clients = (all ? await this.getAllConnectedClients() : await this.getConnectedClients()).sort();
const c = this.occupancy_sensor_service.getCharacteristic(all ?
this.platform.custom.FullWiFiClientList : this.platform.custom.WiFiClientList)!;
if (JSON.stringify({clients}) !== JSON.stringify(c.value)) c.updateValue({clients});
if (JSON.stringify({clients}) !== JSON.stringify(c.value)) c.updateValue({clients} as any);
}, 1000);
}
......
import EventEmitter from 'events';
import {EventEmitter} from 'events';
import Client from 'node-acp';
import {DeviceConfiguration, DEFAULT_ACP_PORT} from './configuration';
......
......@@ -10,6 +10,7 @@ export interface DeviceConfiguration {
port?: number;
password: string;
encryption?: boolean;
accessory?: boolean;
}
export const DEFAULT_ACP_PORT = 5009;
......
export default function({Service, Characteristic, Formats, Perms}: typeof import('hap-nodejs')) {
export default function({Service, Characteristic, Formats, Perms}: typeof import('hap-nodejs')): typeof CustomHapTypes {
/**
* Characteristic "Wi-Fi Satellite Status"
*/
......@@ -9,7 +9,7 @@ export default function({Service, Characteristic, Formats, Perms}: typeof import
static readonly CONNECTED = 1;
static readonly NOT_CONNECTED = 2;
static readonly UUID: string = '0000021E-0000-1000-8000-0026BB765291';
static readonly UUID = '0000021E-0000-1000-8000-0026BB765291';
constructor() {
super('Wi-Fi Satellite Status', WiFiSatelliteStatus.UUID);
......@@ -29,7 +29,7 @@ export default function({Service, Characteristic, Formats, Perms}: typeof import
*/
class WiFiSatellite extends Service {
static readonly UUID: string = '0000020F-0000-1000-8000-0026BB765291';
static readonly UUID = '0000020F-0000-1000-8000-0026BB765291';
constructor(displayName?: string, subtype?: string) {
super(displayName!, WiFiSatellite.UUID, subtype!);
......@@ -44,7 +44,7 @@ export default function({Service, Characteristic, Formats, Perms}: typeof import
*/
class WiFiClientList extends Characteristic {
static readonly UUID: string = '6535A829-0813-4155-A0CD-760E93430203';
static readonly UUID = '6535A829-0813-4155-A0CD-760E93430203';
constructor() {
super('Wi-Fi Client List', WiFiClientList.UUID);
......@@ -62,7 +62,7 @@ export default function({Service, Characteristic, Formats, Perms}: typeof import
*/
class FullWiFiClientList extends Characteristic {
static readonly UUID: string = '6535A82A-0813-4155-A0CD-760E93430203';
static readonly UUID = '6535A82A-0813-4155-A0CD-760E93430203';
constructor() {
super('Full Wi-Fi Client List', FullWiFiClientList.UUID);
......@@ -80,7 +80,7 @@ export default function({Service, Characteristic, Formats, Perms}: typeof import
*/
class WiFiClients extends Service {
static readonly UUID: string = '6535A828-0813-4155-A0CD-760E93430203';
static readonly UUID = '6535A828-0813-4155-A0CD-760E93430203';
constructor(displayName?: string, subtype?: string) {
super(displayName!, WiFiClients.UUID, subtype!);
......@@ -100,70 +100,59 @@ export default function({Service, Characteristic, Formats, Perms}: typeof import
WiFiClientList,
FullWiFiClientList,
WiFiClients,
} as any;
};
}
// @ts-ignore
return;
declare namespace CustomHapTypes {
const Service: typeof import('hap-nodejs').Service;
const Characteristic: typeof import('hap-nodejs').Characteristic;
type Service = import('hap-nodejs').Service;
type Characteristic = import('hap-nodejs').Characteristic;
import {Service, Characteristic} from 'hap-nodejs';
export {};
declare class WiFiSatelliteStatus extends Characteristic {
// The value property of WiFiSatelliteStatus must be one of the following:
static readonly UNKNOWN = 0;
static readonly CONNECTED = 1;
static readonly NOT_CONNECTED = 2;
export class WiFiSatelliteStatus extends Characteristic {
// The value property of WiFiSatelliteStatus must be one of the following:
static readonly UNKNOWN = 0;
static readonly CONNECTED = 1;
static readonly NOT_CONNECTED = 2;
static readonly UUID = '0000021E-0000-1000-8000-0026BB765291';
static readonly UUID = '0000021E-0000-1000-8000-0026BB765291';
constructor();
}
constructor();
}
declare class WiFiSatellite extends Service {
static readonly UUID = '0000020F-0000-1000-8000-0026BB765291';
export class WiFiSatellite extends Service {
static readonly UUID = '0000020F-0000-1000-8000-0026BB765291';
constructor(displayName?: string, subtype?: string);
}
constructor(displayName?: string, subtype?: string);
}
declare class WiFiClientList extends Characteristic {
static readonly UUID = '6535A829-0813-4155-A0CD-760E93430203';
export class WiFiClientList extends Characteristic {
static readonly UUID = '6535A829-0813-4155-A0CD-760E93430203';
constructor();
}
constructor();
}
/**
* Characteristic "Full Wi-Fi Client List"
*/
/**
* Characteristic "Full Wi-Fi Client List"
*/
declare class FullWiFiClientList extends Characteristic {
static readonly UUID = '6535A82A-0813-4155-A0CD-760E93430203';
export class FullWiFiClientList extends Characteristic {
static readonly UUID = '6535A82A-0813-4155-A0CD-760E93430203';
constructor();
}
constructor();
}
/**
* Service "Wi-Fi Clients"
*/
/**
* Service "Wi-Fi Clients"
*/
declare class WiFiClients extends Service {
static readonly UUID = '6535A828-0813-4155-A0CD-760E93430203';
export class WiFiClients extends Service {
static readonly UUID = '6535A828-0813-4155-A0CD-760E93430203';
constructor(displayName?: string, subtype?: string);
constructor(displayName?: string, subtype?: string);
}
}
export type Namespaces = {
WiFiSatelliteStatus: typeof WiFiSatelliteStatus;
WiFiSatellite: typeof WiFiSatellite;
WiFiClientList: typeof WiFiClientList;
FullWiFiClientList: typeof FullWiFiClientList;
WiFiClients: typeof WiFiClients;
};
export type Types = {
WiFiSatelliteStatus: WiFiSatelliteStatus;
WiFiSatellite: WiFiSatellite;
WiFiClientList: WiFiClientList;
FullWiFiClientList: FullWiFiClientList;
WiFiClients: WiFiClients;
};
export {CustomHapTypes};
import AirPort from '../platform';
import {Configuration} from '../configuration';
import {Accessory, Service, Characteristic} from 'hap-nodejs';
import {Accessory, Service} from 'hap-nodejs';
type Logger = typeof console & typeof console.log;
type AccessoryConstructor = any;
......@@ -54,7 +54,7 @@ class AirPortPlatform implements Platform {
accessories(callback: (accessories: {
name: string;
getServices: () => Service[];
accessory: Accessory,
accessory: Accessory;
}[]) => void) {
this.platform.getAccessories().then(accessories => {
const {Service, Characteristic} = this.platform.hap;
......
......@@ -4,7 +4,7 @@ import Accessory, {BaseStationAccessory, types} from './accessories';
export default class AirPort {
readonly hap: typeof import('hap-nodejs');
readonly custom: import('./custom-hap').Namespaces;
readonly custom: typeof import('./custom-hap').CustomHapTypes;
readonly config: Readonly<Configuration>;
readonly clients: Record<DeviceIdentifier, AirPortBaseStation> = {};
......@@ -37,6 +37,7 @@ export default class AirPort {
for (const [identifier, client] of Object.entries(this.clients)) {
if (this.accessories.find(a => a instanceof BaseStationAccessory && a.basestation === client)) continue;
if ('accessory' in client.config && !client.config.accessory) continue;
this.accessories.push(new BaseStationAccessory(this, client, identifier));
}
......
import path from 'path';
import fs from 'fs';
// @ts-ignore
import storage from 'node-persist';
import * as path from 'path';
import * as fs from 'fs';
import * as storage from 'node-persist';
import * as hap from 'hap-nodejs';
import {Bridge, Accessory, Service, Characteristic, uuid} from 'hap-nodejs';
import yaml from 'yaml';
import * as yaml from 'yaml';
import homebridge_plugin from '..';
......@@ -90,6 +89,7 @@ if (!platform_types[config.platform]) {
storage.initSync({
dir: path.resolve(__dirname, '..', '..', 'persist'),
// @ts-ignore
stringify: (data: string) => JSON.stringify(data, undefined, 4) + '\n',
});
......
......@@ -6,7 +6,6 @@
"strict": true,
"declaration": true,
"sourceMap": true,
"downlevelIteration": true,
"experimentalDecorators": true,
"rootDir": "src",
"outDir": "dist"
......
declare module 'bonjour-hap' {
export const BonjourHap: any;
export const MulticastOptions: any;
export const Service: any;
export type BonjourHap = any;
export type MulticastOptions = any;
export type Service = any;
}
declare module 'fast-srp-hap' {
export const Server: any;
export const SrpParams: any;
export type Server = any;
export type SrpParams = any;
}