Commit 75428c6f authored by Samuel Elliott's avatar Samuel Elliott

Add support for use as a platform and warn when multiple Television services...

Add support for use as a platform and warn when multiple Television services are published on the same bridge
parent 242f80a8
Vestel TV Network Remote plugin for Homebridge
===
Homebridge plugin for controlling Vestel smart TVs. [Vestel manufactures TVs as a lot of brand names so if your TV is from a smaller brand or a store's own brand it's probably worth checking if it's a Vestel TV.](https://www.avforums.com/threads/how-to-tell-if-your-tvs-a-vestel.2019864/)
Homebridge plugin for controlling Vestel smart TVs.
[Vestel manufactures TVs as a lot of brand names so if your TV is from a smaller brand or a store's own brand it's probably worth checking if it's a Vestel TV.](https://www.avforums.com/threads/how-to-tell-if-your-tvs-a-vestel.2019864/)
[(I wouldn't recommend these TVs as they have no security at all.)](https://www.av-comparatives.org/security-flaws-firmware-used-30-popular-tv-brands/)
[(I wouldn't recommend these TVs as they have no security protections at all. Use them on an isolated network if possible.)](https://www.av-comparatives.org/security-flaws-firmware-used-30-popular-tv-brands/)
```
npm install -g homebridge
......@@ -17,7 +18,8 @@ npm install -g git+https://gitlab.fancy.org.uk/samuel/homebridge-vestel-network-
}
```
The TV generates a new UUID every time it is turned on so there's no way of discovering the IP address so you need to set a static IP address on the TV.
The TV generates a new UUID every time it is turned on so there's no way of discovering the IP address so you need to
set a static IP address on the TV.
### Features
......@@ -27,7 +29,37 @@ The TV generates a new UUID every time it is turned on so there's no way of disc
- Setting current input/app/channel
- Sending remote keys
[Apparently Vestel TVs support a lot more commands.](https://forum.digitalfernsehen.de/threads/medion-md-30465-42-led-backlight-tv-life%C2%AE-x17006.276321/page-64#post-7474289) I haven't tested all of these.
[Apparently Vestel TVs support a lot more commands.](https://forum.digitalfernsehen.de/threads/medion-md-30465-42-led-backlight-tv-life%C2%AE-x17006.276321/page-64#post-7474289)
I haven't tested all of these.
### Multiple TVs
If you have multiple TVs they should be published separately as the iOS TV Remote only allows one Television service
per HAP server. You'll have to use the platform to do this instead of the accessory. Each object in the `tvs` array
should be the same as if you were using the accessory type, but without the `accessory` property.
```json
{
"platform": "vestel-network-remote.TVs",
"tvs": [
{
"name": "Samuel's Bedroom TV",
"ip_address": "192.168.3.223"
}
]
}
```
The first TV will be published on the bridge and any additional TVs will be published separately. If you have a TV
from another plugin on the bridge you'll have to set the `external` property explicitly.
```json
{
"platform": "vestel-network-remote.TVs",
"external": true,
"tvs": [...]
}
```
Turning the TV on
---
......@@ -36,7 +68,8 @@ To turn the TV on you need another device capable of turning the TV on with HDMI
### Apple TV 4th generation
Using an Apple TV 4th generation requires `node-appletv`. This isn't listed as a dependency so you need to install it seperately.
Using an Apple TV 4th generation requires `node-appletv`. This isn't listed as a dependency so you need to install it
seperately.
```
# https://github.com/socalrds/node-appletv uses a working sodium
......@@ -64,7 +97,9 @@ npm install -g socalrds/node-appletv
Controlling volume
---
If your TV exposes a RenderingControl service you can control the volume. By default this will use `"http://{ip_address}:2869/dmr.xml"` - if it's different on your TV or it doesn't have a RenderingControl service you can change the location of the MediaRenderer description or set it to `null`.
If your TV exposes a RenderingControl service you can control the volume. By default this will use
`"http://{ip_address}:2869/dmr.xml"` - if it's different on your TV or it doesn't have a RenderingControl service you
can change the location of the MediaRenderer description or set it to `null`.
```json
{
......@@ -75,7 +110,8 @@ If your TV exposes a RenderingControl service you can control the volume. By def
}
```
This will add a Speaker service. As the Speaker service is not supported by the Home app, you can also choose to use a Lightbulb service instead of or in addition to the Speaker service.
This will add a Speaker service. As the Speaker service is not supported by the Home app, you can also choose to use
a Lightbulb service instead of or in addition to the Speaker service.
```json
{
......@@ -90,7 +126,8 @@ This will add a Speaker service. As the Speaker service is not supported by the
iOS 12.2 Television service
---
You can enable the Television service (supported in the Home app on iOS 12.2 and later) by setting the `expose_television_service` flag.
You can enable the Television service (supported in the Home app on iOS 12.2 and later) by setting the
`expose_television_service` flag.
I haven't updated to the iOS 12.2 beta so I haven't tested this properly.
......@@ -115,7 +152,12 @@ I haven't updated to the iOS 12.2 beta so I haven't tested this properly.
}
```
The `tv_sources` object defines how the sources should be switched. The key is the internal identifier of the source. The value is either an array (the first item can be anything you can use to reference the source ("HDMI 1" is preferred over "Apple TV" for example as you can set your own name in the Home app), the second item being the name of an [Input Source Type](https://github.com/KhaosT/HAP-NodeJS/blob/master/lib/gen/HomeKitTypes-Television.js) and the third being the name of an [Input Device Type](https://github.com/KhaosT/HAP-NodeJS/blob/master/lib/gen/HomeKitTypes-Television.js)) or an object:
The `tv_sources` object defines how the sources should be switched. The key is the internal identifier of the source.
The value is either an array (the first item can be anything you can use to reference the source ("HDMI 1" is
preferred over "Apple TV" for example as you can set your own name in the Home app), the second item being the name
of an [Input Source Type](https://github.com/KhaosT/HAP-NodeJS/blob/master/lib/gen/HomeKitTypes-Television.js) and the
third being the name of an
[Input Device Type](https://github.com/KhaosT/HAP-NodeJS/blob/master/lib/gen/HomeKitTypes-Television.js)) or an object:
```json
...,
......@@ -127,7 +169,8 @@ The `tv_sources` object defines how the sources should be switched. The key is t
...
```
You can find the internal identifier of the source by trying different identifiers. These are incremental and start from `0` but include sources for other hardware models, so some do nothing.
You can find the internal identifier of the source by trying different identifiers. These are incremental and start
from `0` but include sources for other hardware models, so some do nothing.
```
npm install -g git+https://gitlab.fancy.org.uk/samuel/vestel-network-remote
......@@ -148,9 +191,12 @@ You can also set the picture zoom after changing the source, as this is sometime
[Values for `picture_zoom` can be found here.](https://gitlab.fancy.org.uk/samuel/vestel-network-remote/blob/v0.3.1/src/followtv.js#L243)
The `apple_tv_input` property should be set to the index of the input the Apple TV used to turn the TV on is connected to. This is used to display the correct source after turning the TV on with an Apple TV (which should set the source to the Apple TV). The default is the first HDMI source.
The `apple_tv_input` property should be set to the index of the input the Apple TV used to turn the TV on is connected
to. This is used to display the correct source after turning the TV on with an Apple TV (which should set the source
to the Apple TV). The default is the first HDMI source.
The `default_input` property should be set to the index of the input to display when the plugin doesn't know what source is currently active. The default is 0 (the first source).
The `default_input` property should be set to the index of the input to display when the plugin doesn't know what
source is currently active. The default is 0 (the first source).
Enabling the Television service will disable the Switch and Speaker service. Set their flags explicitly to enable them.
......@@ -162,10 +208,14 @@ Enabling the Television service will disable the Switch and Speaker service. Set
"expose_television_service": true,
"tv_sources": ...,
"expose_power_service": true,
"expose_speaker_service": true
"expose_speaker_lightbulb_service": true
}
```
Enabling the Television service also enables a TelevisionSpeaker service, which is exactly the same as the Speaker
service but has an additional VolumeControlType characteristic. Don't enable both speaker services as they use the
same UUID.
### TV apps
You can add TV apps by adding them to the `tv_apps` object.
......
......@@ -22,7 +22,9 @@ module.exports = function (homebridge) {
console.log('node-appletv is not installed. You will not be able to use an Apple TV to turn a TV on.');
}
homebridge.registerAccessory('vestel-network-remote', 'TV', TVAccessory.use(homebridge, storage));
const BoundTVAccessory = TVAccessory.use(homebridge, homebridge.hap, storage);
homebridge.registerAccessory('vestel-network-remote', 'TV', BoundTVAccessory);
homebridge.registerPlatform('vestel-network-remote', 'TVs', TVPlatform.use(BoundTVAccessory, homebridge, homebridge.hap, storage));
Object.assign(key_map, {
[homebridge.hap.Characteristic.RemoteKey.REWIND]: 'REWIND',
......@@ -41,24 +43,140 @@ module.exports = function (homebridge) {
});
};
class TVAccessory {
static use(homebridge, storage) {
return class extends TVAccessory {
class TVPlatform {
static use(TVAccessory, homebridge, hap, storage) {
return class extends this {
static get TVAccessory() {
return TVAccessory;
}
static get homebridge_api() {
return homebridge;
}
static get hap() {
return hap;
}
static get storage() {
return storage;
}
};
}
static get hap() {
return this.homebridge_api.hap;
constructor(log, config, homebridge) {
this.log = log;
this.config = config;
if (this.constructor.homebridge_api !== homebridge) {
throw new Error('Different homebridge API???');
}
this.tvs = config.tvs || [];
this.external = config.external !== undefined ? !!config.external : false;
}
accessories(callback) {
this.getAccessories().then(accessories => callback(accessories)).catch(err => {
this.log.error('Failed to load platform accessories', err);
callback([]);
});
}
constructor(log, config) {
/**
* Get accessories to publish on the bridge and publish any external accessories.
*
* @return {Promise<this.constructor.homebridge_api.PlatformAccessory[]>}
*/
async getAccessories() {
this.getAccessories = () => {
throw new Error('getAccessories can only be called once');
};
const accessories = [];
const external_accessories = [];
for (const config of this.tvs) {
const accessory = new this.constructor.TVAccessory(this.log, config, this);
const platform_accessory = this.createPlatformAccessory(accessory);
const external = this.external || (config.external !== undefined ? !!config.external :
!!this.constructor.TVAccessory.instances.find(a => a.services.find(s =>
s.UUID === this.constructor.hap.Service.Television.UUID)));
if (!this.constructor.TVAccessory.instances) this.constructor.TVAccessory.instances = [];
if (this.constructor.TVAccessory.instances.find(a => a.expose_television_service))) {
this.log.warn('You have multiple TV accessories published on the same bridge. The iOS TV Remote ' +
'only allows one Television service per HAP server. You should set the external flag on at ' +
'least all but one to true or remove it.');
}
this.constructor.TVAccessory.instances.push(accessory);
external ? external_accessories.push(platform_accessory) : accessories.push(platform_accessory);
}
for (const platform_accessory of external_accessories) {
this.constructor.homebridge_api.publishExternalAccessories(platform_accessory);
}
return accessories;
}
/**
* Creates a PlatformAccessory from a TVAccessory.
*
* @param {TVAccessory} accessory
* @return {PlatformAccessory}
*/
createPlatformAccessory(accessory) {
const platform_accessory = new this.constructor.homebridge_api.platformAccessory(
accessory.name,
this.constructor.hap.uuid.generate('vestel-network-remote.TV:' + accessory.name),
this.constructor.hap.Accessory.Categories.TELEVISION
);
const services = accessory.getServices();
for (const service of services) {
if (service instanceof Service.AccessoryInformation) {
for (const characteristic of [
'Manufacturer', 'Model', 'SerialNumber', 'FirmwareRevision', 'HardwareRevision',
]) {
const {value} = service.getCharacteristic(Characteristic[characteristic]);
if (value) platform_accessory.getService(Service.AccessoryInformation).setCharacteristic(Characteristic[characteristic], value);
}
continue;
}
platform_accessory.addService(service);
}
return platform_accessory;
}
}
class TVAccessory {
static use(homebridge, hap, storage) {
return class extends this {
static get homebridge_api() {
return homebridge;
}
static get hap() {
return hap;
}
static get storage() {
return storage;
}
};
}
constructor(log, config, platform_instance) {
this.platform_instance = platform_instance;
this.log = log;
this.name = config.name;
this.mac_address = config.mac_address;
......@@ -70,6 +188,16 @@ class TVAccessory {
this.storage_key = 'vestel-network-remote.TV.' + this.name + '.json';
if (!this.constructor.instances) this.constructor.instances = [];
if (this.constructor.instances.find(a => !a.platform_instance && a.expose_television_service)) {
this.log.warn('You have multiple TV accessories published on the same bridge. The iOS TV Remote only ' +
'allows one Television service per HAP server. You should move at least all but one TV accessory to ' +
'the vestel-network-remote.TVs platform.');
}
this.constructor.instances.push(this);
// Services
this.expose_television_service = config.expose_television_service !== undefined ?
config.expose_television_service : !!config.tv_sources;
......@@ -659,5 +787,6 @@ class TVAccessory {
}
}
module.exports.TVPlatform = TVPlatform;
module.exports.TVAccessory = TVAccessory;
module.exports.key_map = key_map;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment