added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.nuvo</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@@ -0,0 +1,397 @@
# Nuvo Grand Concerto & Essentia G Binding
![Nuvo logo](doc/nuvo_logo.png)
This binding can be used to control the Nuvo Grand Concerto or Essentia G whole house multi-zone amplifier.
Up to 20 keypad zones can be controlled when zone expansion modules are used (if not all zones on the amp are used they can be excluded via configuration).
The binding supports two different kinds of connections:
* serial connection,
* serial over IP connection
For users without a serial connector on the server side, you can use a serial to USB adapter.
You don't need to have your Grand Concerto or Essentia G whole house amplifier device directly connected to your openHAB server.
You can connect it for example to a Raspberry Pi and use [ser2net Linux tool](https://sourceforge.net/projects/ser2net/) to make the serial connection available on LAN (serial over IP).
## Supported Things
There is exactly one supported thing type, which represents the amplifier controller.
It has the `amplifier` id.
## Discovery
Discovery is not supported.
You have to add all things manually.
## Binding Configuration
There are no overall binding configuration settings that need to be set.
All settings are through thing configuration parameters.
## Thing Configuration
The thing has the following configuration parameters:
| Parameter Label | Parameter ID | Description | Accepted values |
|-------------------------|--------------|------------------------------------------------------------------------------------------------------------------------------------|------------------------|
| Serial Port | serialPort | Serial port to use for connecting to the Nuvo whole house amplifier device | a comm port name |
| Address | host | Host name or IP address of the machine connected to the Nuvo whole house amplifier device (serial over IP) | host name or ip |
| Port | port | Communication port (serial over IP). | ip port number |
| Number of Zones | numZones | (Optional) Number of zones on the amplifier to utilize in the binding (up to 20 zones when zone expansion modules are used) | (1-20; default 6) |
| Sync Clock on GConcerto | clockSync | (Optional) If set to true, the binding will sync the internal clock on the Grand Concerto to match the openHAB host's system clock | Boolean; default false |
Some notes:
* If a zone has a maximum volume limit configured by the Nuvo configurator, the volume slider will automatically drop back to that level if set above the configured limit.
* Source display_line1 thru 4 can only be updated on non NuvoNet sources.
* The track_position channel does not update continuously for NuvoNet sources. It only changes when the track changes or playback is paused/unpaused.
* On Linux, you may get an error stating the serial port cannot be opened when the Nuvo binding tries to load.
* You can get around this by adding the `openhab` user to the `dialout` group like this: `usermod -a -G dialout openhab`.
* Also on Linux you may have issues with the USB if using two serial USB devices e.g. Nuvo and RFXcom. See the [general documentation about serial port configuration](/docs/administration/serial.html) for more on symlinking the USB ports.
* Here is an example of ser2net.conf you can use to share your serial port /dev/ttyUSB0 on IP port 4444 using [ser2net Linux tool](https://sourceforge.net/projects/ser2net/) (take care, the baud rate is specific to the Nuvo amplifier):
```
4444:raw:0:/dev/ttyUSB0:57600 8DATABITS NONE 1STOPBIT LOCAL
```
## Channels
The following channels are available:
| Channel ID | Item Type | Description |
|--------------------------------------|-------------|---------------------------------------------------------------------------------------------------------------|
| system#alloff | Switch | Turn all zones off simultaneously |
| system#allmute | Switch | Mute or unmute all zones simultaneously |
| system#page | Switch | Turn on or off the Page All Zones feature (while on the amplifier switches to source 6) |
| zoneN#power (where N= 1-20) | Switch | Turn the power for a zone on or off |
| zoneN#source (where N= 1-20) | Number | Select the source input for a zone (1-6) |
| zoneN#volume (where N= 1-20) | Dimmer | Control the volume for a zone (0-100%) [translates to 0-79] |
| zoneN#mute (where N= 1-20) | Switch | Mute or unmute a zone |
| zoneN#control (where N= 1-20) | Player | Simulate pressing the transport control buttons on the keypad e.g. play/pause/next/previous |
| zoneN#treble (where N= 1-20) | Number | Adjust the treble control for a zone (-18 to 18 [in increments of 2]) -18=none, 0=flat, 18=full |
| zoneN#bass (where N= 1-20) | Number | Adjust the bass control for a zone (-18 to 18 [in increments of 2]) -18=none, 0=flat, 18=full |
| zoneN#balance (where N= 1-20) | Number | Adjust the balance control for a zone (-18 to 18 [in increments of 2]) -18=left, 0=center, 18=right |
| zoneN#loudness (where N= 1-20) | Switch | Turn on or off the loudness compensation setting for the zone |
| zoneN#dnd (where N= 1-20) | Switch | Turn on or off the Do Not Disturb for the zone (for when the amplifiers's Page All Zones feature is activated)|
| zoneN#lock (where N= 1-20) | Contact | Indicates if this zone is currently locked |
| zoneN#party (where N= 1-20) | Switch | Turn on or off the party mode feature with this zone as the host |
| sourceN#display_line1 (where N= 1-6) | String | 1st line of text being displayed on the keypad. Can be updated for a non NuvoNet source |
| sourceN#display_line2 (where N= 1-6) | String | 2nd line of text being displayed on the keypad. Can be updated for a non NuvoNet source |
| sourceN#display_line3 (where N= 1-6) | String | 3rd line of text being displayed on the keypad. Can be updated for a non NuvoNet source |
| sourceN#display_line4 (where N= 1-6) | String | 4th line of text being displayed on the keypad. Can be updated for a non NuvoNet source |
| sourceN#play_mode (where N= 1-6) | String | The current playback mode of the source, ie: Playing, Paused, etc. (ReadOnly) See rules example for updating |
| sourceN#track_length (where N= 1-6) | Number:Time | The total running time of the current playing track (ReadOnly) See rules example for updating |
| sourceN#track_position (where N= 1-6)| Number:Time | The running time elapsed of the current playing track (ReadOnly) See rules example for updating |
| sourceN#button_press (where N= 1-6) | String | Indicates the last button pressed on the keypad for a non NuvoNet source (ReadOnly) |
## Full Example
nuvo.things:
```java
//serial port connection
nuvo:amplifier:myamp "Nuvo WHA" [ serialPort="COM5", numZones=6, clockSync=false]
// serial over IP connection
nuvo:amplifier:myamp "Nuvo WHA" [ host="192.168.0.10", port=4444, numZones=6, clockSync=false]
```
nuvo.items:
```java
// system
Switch nuvo_system_alloff "All Zones Off" { channel="nuvo:amplifier:myamp:system#alloff" }
Switch nuvo_system_allmute "All Zones Mute" { channel="nuvo:amplifier:myamp:system#allmute" }
Switch nuvo_system_page "Page All Zones" { channel="nuvo:amplifier:myamp:system#page" }
// zones
Switch nuvo_z1_power "Power" { channel="nuvo:amplifier:myamp:zone1#power" }
Number nuvo_z1_source "Source Input [%s]" { channel="nuvo:amplifier:myamp:zone1#source" }
Dimmer nuvo_z1_volume "Volume [%d %%]" { channel="nuvo:amplifier:myamp:zone1#volume" }
Switch nuvo_z1_mute "Mute" { channel="nuvo:amplifier:myamp:zone1#mute" }
Player nuvo_z1_control "Control" { channel="nuvo:amplifier:myamp:zone1#control" }
Number nuvo_z1_treble "Treble Adjustment [%s]" { channel="nuvo:amplifier:myamp:zone1#treble" }
Number nuvo_z1_bass "Bass Adjustment [%s]" { channel="nuvo:amplifier:myamp:zone1#bass" }
Number nuvo_z1_balance "Balance Adjustment [%s]" { channel="nuvo:amplifier:myamp:zone1#balance" }
Switch nuvo_z1_loudness "Loudness" { channel="nuvo:amplifier:myamp:zone1#loudness" }
Switch nuvo_z1_dnd "Do Not Disturb" { channel="nuvo:amplifier:myamp:zone1#dnd" }
Switch nuvo_z1_lock "Zone Locked [%s]" { channel="nuvo:amplifier:myamp:zone1#lock" }
Switch nuvo_z1_party "Party Mode" { channel="nuvo:amplifier:myamp:zone1#party" }
// > repeat for zones 2-20 (substitute z1 and zone1) < //
// sources
String nuvo_s1_display_line1 "Line 1: [%s]" { channel="nuvo:amplifier:myamp:source1#display_line1" }
String nuvo_s1_display_line2 "Line 2: [%s]" { channel="nuvo:amplifier:myamp:source1#display_line2" }
String nuvo_s1_display_line3 "Line 3: [%s]" { channel="nuvo:amplifier:myamp:source1#display_line3" }
String nuvo_s1_display_line4 "Line 4: [%s]" { channel="nuvo:amplifier:myamp:source1#display_line4" }
String nuvo_s1_play_mode "Play Mode: [%s]" { channel="nuvo:amplifier:myamp:source1#play_mode" }
Number:Time nuvo_s1_track_length "Track Length: [%s s]" { channel="nuvo:amplifier:myamp:source1#track_length" }
Number:Time nuvo_s1_track_position "Track Position: [%s s]" { channel="nuvo:amplifier:myamp:source1#track_position" }
String nuvo_s1_button_press "Button: [%s]" { channel="nuvo:amplifier:myamp:source1#button_press" }
String nuvo_s2_display_line1 "Line 1: [%s]" { channel="nuvo:amplifier:myamp:source2#display_line1" }
String nuvo_s2_display_line2 "Line 2: [%s]" { channel="nuvo:amplifier:myamp:source2#display_line2" }
String nuvo_s2_display_line3 "Line 3: [%s]" { channel="nuvo:amplifier:myamp:source2#display_line3" }
String nuvo_s2_display_line4 "Line 4: [%s]" { channel="nuvo:amplifier:myamp:source2#display_line4" }
String nuvo_s2_play_mode "Play Mode: [%s]" { channel="nuvo:amplifier:myamp:source2#play_mode" }
Number:Time nuvo_s2_track_length "Track Length: [%s s]" { channel="nuvo:amplifier:myamp:source2#track_length" }
Number:Time nuvo_s2_track_position "Track Position: [%s s]" { channel="nuvo:amplifier:myamp:source2#track_position" }
String nuvo_s2_button_press "Button: [%s]" { channel="nuvo:amplifier:myamp:source2#button_press" }
String nuvo_s3_display_line1 "Line 1: [%s]" { channel="nuvo:amplifier:myamp:source3#display_line1" }
String nuvo_s3_display_line2 "Line 2: [%s]" { channel="nuvo:amplifier:myamp:source3#display_line2" }
String nuvo_s3_display_line3 "Line 3: [%s]" { channel="nuvo:amplifier:myamp:source3#display_line3" }
String nuvo_s3_display_line4 "Line 4: [%s]" { channel="nuvo:amplifier:myamp:source3#display_line4" }
String nuvo_s3_play_mode "Play Mode: [%s]" { channel="nuvo:amplifier:myamp:source3#play_mode" }
Number:Time nuvo_s3_track_length "Track Length: [%s s]" { channel="nuvo:amplifier:myamp:source3#track_length" }
Number:Time nuvo_s3_track_position "Track Position: [%s s]" { channel="nuvo:amplifier:myamp:source3#track_position" }
String nuvo_s3_button_press "Button: [%s]" { channel="nuvo:amplifier:myamp:source3#button_press" }
String nuvo_s4_display_line1 "Line 1: [%s]" { channel="nuvo:amplifier:myamp:source4#display_line1" }
String nuvo_s4_display_line2 "Line 2: [%s]" { channel="nuvo:amplifier:myamp:source4#display_line2" }
String nuvo_s4_display_line3 "Line 3: [%s]" { channel="nuvo:amplifier:myamp:source4#display_line3" }
String nuvo_s4_display_line4 "Line 4: [%s]" { channel="nuvo:amplifier:myamp:source4#display_line4" }
String nuvo_s4_play_mode "Play Mode: [%s]" { channel="nuvo:amplifier:myamp:source4#play_mode" }
Number:Time nuvo_s4_track_length "Track Length: [%s s]" { channel="nuvo:amplifier:myamp:source4#track_length" }
Number:Time nuvo_s4_track_position "Track Position: [%s s]" { channel="nuvo:amplifier:myamp:source4#track_position" }
String nuvo_s4_button_press "Button: [%s]" { channel="nuvo:amplifier:myamp:source4#button_press" }
String nuvo_s5_display_line1 "Line 1: [%s]" { channel="nuvo:amplifier:myamp:source5#display_line1" }
String nuvo_s5_display_line2 "Line 2: [%s]" { channel="nuvo:amplifier:myamp:source5#display_line2" }
String nuvo_s5_display_line3 "Line 3: [%s]" { channel="nuvo:amplifier:myamp:source5#display_line3" }
String nuvo_s5_display_line4 "Line 4: [%s]" { channel="nuvo:amplifier:myamp:source5#display_line4" }
String nuvo_s5_play_mode "Play Mode: [%s]" { channel="nuvo:amplifier:myamp:source5#play_mode" }
Number:Time nuvo_s5_track_length "Track Length: [%s s]" { channel="nuvo:amplifier:myamp:source5#track_length" }
Number:Time nuvo_s5_track_position "Track Position: [%s s]" { channel="nuvo:amplifier:myamp:source5#track_position" }
String nuvo_s5_button_press "Button: [%s]" { channel="nuvo:amplifier:myamp:source5#button_press" }
String nuvo_s6_display_line1 "Line 1: [%s]" { channel="nuvo:amplifier:myamp:source6#display_line1" }
String nuvo_s6_display_line2 "Line 2: [%s]" { channel="nuvo:amplifier:myamp:source6#display_line2" }
String nuvo_s6_display_line3 "Line 3: [%s]" { channel="nuvo:amplifier:myamp:source6#display_line3" }
String nuvo_s6_display_line4 "Line 4: [%s]" { channel="nuvo:amplifier:myamp:source6#display_line4" }
String nuvo_s6_play_mode "Play Mode: [%s]" { channel="nuvo:amplifier:myamp:source6#play_mode" }
Number:Time nuvo_s6_track_length "Track Length: [%s s]" { channel="nuvo:amplifier:myamp:source6#track_length" }
Number:Time nuvo_s6_track_position "Track Position: [%s s]" { channel="nuvo:amplifier:myamp:source6#track_position" }
String nuvo_s6_button_press "Button: [%s]" { channel="nuvo:amplifier:myamp:source6#button_press" }
```
nuvo.sitemap:
```perl
sitemap nuvo label="Audio Control" {
Frame label="System" {
Switch item=nuvo_system_alloff mappings=[ON=" "]
Switch item=nuvo_system_allmute
Switch item=nuvo_system_page
}
Frame label="Zone 1"
Switch item=nuvo_z1_power visibility=[nuvo_z1_lock!="1"]
Selection item=nuvo_z1_source visibility=[nuvo_z1_power==ON] icon="player"
//Volume can be a Setpoint also
Slider item=nuvo_z1_volume minValue=0 maxValue=100 step=1 visibility=[nuvo_z1_power==ON] icon="soundvolume"
Switch item=nuvo_z1_mute visibility=[nuvo_z1_power==ON] icon="soundvolume_mute"
Default item=nuvo_z1_control visibility=[nuvo_z1_power==ON]
Text item=nuvo_s1_display_line1 visibility=[nuvo_z1_source=="1"] icon="zoom"
Text item=nuvo_s1_display_line2 visibility=[nuvo_z1_source=="1"] icon="zoom"
Text item=nuvo_s1_display_line3 visibility=[nuvo_z1_source=="1"] icon="zoom"
Text item=nuvo_s1_display_line4 visibility=[nuvo_z1_source=="1"] icon="zoom"
Text item=nuvo_s1_play_mode visibility=[nuvo_z1_source=="1"] icon="player"
Text item=nuvo_s1_track_length visibility=[nuvo_z1_source=="1"]
Text item=nuvo_s1_track_position visibility=[nuvo_z1_source=="1"]
Text item=nuvo_s1_button_press visibility=[nuvo_z1_source=="1"] icon="none"
Text item=nuvo_s2_display_line1 visibility=[nuvo_z1_source=="2"] icon="zoom"
Text item=nuvo_s2_display_line2 visibility=[nuvo_z1_source=="2"] icon="zoom"
Text item=nuvo_s2_display_line3 visibility=[nuvo_z1_source=="2"] icon="zoom"
Text item=nuvo_s2_display_line4 visibility=[nuvo_z1_source=="2"] icon="zoom"
Text item=nuvo_s2_play_mode visibility=[nuvo_z1_source=="2"] icon="player"
Text item=nuvo_s2_track_length visibility=[nuvo_z1_source=="2"]
Text item=nuvo_s2_track_position visibility=[nuvo_z1_source=="2"]
Text item=nuvo_s2_button_press visibility=[nuvo_z1_source=="2"] icon="none"
Text item=nuvo_s3_display_line1 visibility=[nuvo_z1_source=="3"] icon="zoom"
Text item=nuvo_s3_display_line2 visibility=[nuvo_z1_source=="3"] icon="zoom"
Text item=nuvo_s3_display_line3 visibility=[nuvo_z1_source=="3"] icon="zoom"
Text item=nuvo_s3_display_line4 visibility=[nuvo_z1_source=="3"] icon="zoom"
Text item=nuvo_s3_play_mode visibility=[nuvo_z1_source=="3"] icon="player"
Text item=nuvo_s3_track_length visibility=[nuvo_z1_source=="3"]
Text item=nuvo_s3_track_position visibility=[nuvo_z1_source=="3"]
Text item=nuvo_s3_button_press visibility=[nuvo_z1_source=="3"] icon="none"
Text item=nuvo_s4_display_line1 visibility=[nuvo_z1_source=="4"] icon="zoom"
Text item=nuvo_s4_display_line2 visibility=[nuvo_z1_source=="4"] icon="zoom"
Text item=nuvo_s4_display_line3 visibility=[nuvo_z1_source=="4"] icon="zoom"
Text item=nuvo_s4_display_line4 visibility=[nuvo_z1_source=="4"] icon="zoom"
Text item=nuvo_s4_play_mode visibility=[nuvo_z1_source=="4"] icon="player"
Text item=nuvo_s4_track_length visibility=[nuvo_z1_source=="4"]
Text item=nuvo_s4_track_position visibility=[nuvo_z1_source=="4"]
Text item=nuvo_s4_button_press visibility=[nuvo_z1_source=="4"] icon="none"
Text item=nuvo_s5_display_line1 visibility=[nuvo_z1_source=="5"] icon="zoom"
Text item=nuvo_s5_display_line2 visibility=[nuvo_z1_source=="5"] icon="zoom"
Text item=nuvo_s5_display_line3 visibility=[nuvo_z1_source=="5"] icon="zoom"
Text item=nuvo_s5_display_line4 visibility=[nuvo_z1_source=="5"] icon="zoom"
Text item=nuvo_s5_play_mode visibility=[nuvo_z1_source=="5"] icon="player"
Text item=nuvo_s5_track_length visibility=[nuvo_z1_source=="5"]
Text item=nuvo_s5_track_position visibility=[nuvo_z1_source=="5"]
Text item=nuvo_s5_button_press visibility=[nuvo_z1_source=="5"] icon="none"
Text item=nuvo_s6_display_line1 visibility=[nuvo_z1_source=="6"] icon="zoom"
Text item=nuvo_s6_display_line2 visibility=[nuvo_z1_source=="6"] icon="zoom"
Text item=nuvo_s6_display_line3 visibility=[nuvo_z1_source=="6"] icon="zoom"
Text item=nuvo_s6_display_line4 visibility=[nuvo_z1_source=="6"] icon="zoom"
Text item=nuvo_s6_play_mode visibility=[nuvo_z1_source=="6"] icon="player"
Text item=nuvo_s6_track_length visibility=[nuvo_z1_source=="6"]
Text item=nuvo_s6_track_position visibility=[nuvo_z1_source=="6"]
Text item=nuvo_s6_button_press visibility=[nuvo_z1_source=="6"] icon="none"
Setpoint item=nuvo_z1_treble label="Treble Adjustment [%d]" minValue=-18 maxValue=18 step=2 visibility=[nuvo_z1_power==ON]
Setpoint item=nuvo_z1_bass label="Bass Adjustment [%d]" minValue=-18 maxValue=18 step=2 visibility=[nuvo_z1_power==ON]
Setpoint item=nuvo_z1_balance label="Balance Adjustment [%d]" minValue=-18 maxValue=18 step=2 visibility=[nuvo_z1_power==ON]
Switch item=nuvo_z1_loudness visibility=[nuvo_z1_power==ON]
Switch item=nuvo_z1_dnd visibility=[nuvo_z1_power==ON]
Text item=nuvo_z1_lock label="Zone Locked: [%s]" icon="lock"
Switch item=nuvo_z1_party visibility=[nuvo_z1_power==ON]
}
//repeat for zones 2-20 (substitute z1)
}
```
nuvo.rules:
```java
import java.text.Normalizer
val actions = getActions("nuvo","nuvo:amplifier:myamp")
// send command a custom command to the Nuvo Amplifier
// see 'NuVo Grand Concerto Serial Control Protocol.pdf' for more command examples
// https://www.legrand.us/-/media/brands/nuvo/nuvo/catalog/softwaredownloads-new/i8g_e6g_control_protocol.ashx
// commands send through the binding do not need the leading '*'
rule "Nuvo Custom Command example"
when
Item SomeItemTrigger received command
then
if(null === actions) {
logInfo("actions", "Actions not found, check thing ID")
return
}
// Send a message to Source 3
//actions.sendNuvoCommand("S3MSG\"Hello World\",0,0")
// Send a message to Zone 11
//actions.sendNuvoCommand("Z11MSG\"Hello World\",0,0")
// Select a Favorite (1-12) for Zone 2
//actions.sendNuvoCommand("Z2FAV1")
end
// In the below examples, a method for maintaing Metadata information
// for a hypothetical non NuvoNet Source 3 is demonstrated
// Item_Containing_TrackLength should get a 'received update' when the track changes
// ('changed' is not sufficient if two consecutive tracks are the same length)
rule "Load track play info for Source 3"
when
Item Item_Containing_TrackLength received update
then
if(null === actions) {
logInfo("actions", "Actions not found, check thing ID")
return
}
// strip off any non-numeric characters and multiply seconds by 10 (Nuvo expects tenths of a second)
var int trackLength = Integer::parseInt(Item_Containing_TrackLength.state.toString.replaceAll("[\\D]", "")) * 10
// '0' indicates the track is just starting (at position 0), '2' indicates to Nuvo that the track is playing
// The Nuvo keypad will now begin counting up the elapsed time displayed (starting from 0)
actions.sendNuvoCommand("S3DISPINFO," + trackLength.toString() + ",0,2")
end
rule "Load track name for Source 3"
when
Item Item_Containing_TrackName changed
then
// The Nuvo keypad cannot display extended ASCII characters (accent, umulat, etc.)
// Below we transform extended ASCII chars into their basic counterparts
// example: 'La Touché' becomes 'La Touche' and 'Nöel' becomes 'Noel'
var trackName = Normalizer::normalize(Item_Containing_TrackName.state.toString, Normalizer.Form.NFD).replaceAll("[^\\p{ASCII}]", "")
nuvo_s3_display_line4.sendCommand(trackName)
nuvo_s3_display_line1.sendCommand("")
end
rule "Load album name for Source 3"
when
Item Item_Containing_AlbumName changed
then
// fix extended ASCII chars
var albumName = Normalizer::normalize(Item_Containing_AlbumName.state.toString, Normalizer.Form.NFD).replaceAll("[^\\p{ASCII}]", "")
nuvo_s3_display_line2.sendCommand(albumName)
end
rule "Load artist name for Source 3"
when
Item Item_Containing_ArtistName changed
then
// fix extended ASCII chars
var artistName = Normalizer::normalize(Item_Containing_ArtistName.state.toString, Normalizer.Form.NFD).replaceAll("[^\\p{ASCII}]", "")
nuvo_s3_display_line3.sendCommand(artistName)
end
// In this rule we have three items: Item_Containing_PlayMode, Item_Containing_TrackLength & Item_Containing_TrackPosition
// Item_Containing_PlayMode reports the playing state of the music source as a string such as 'Playing' or 'Paused'
// Item_Containing_TrackLength reports the length of the track in seconds
// Item_Containing_TrackPosition report the current playback position of the track in seconds
rule "Update play state info for Source 3"
when
Item Item_Containing_PlayMode changed
then
var playMode = Item_Containing_PlayMode.state.toString()
// strip off any non-numeric characters and multiply seconds by 10 (Nuvo expects tenths of a second)
var int trackLength = Integer::parseInt(Item_Containing_TrackLength.state.toString.replaceAll("[\\D]", "")) * 10
var int trackPosition = Integer::parseInt(Item_Containing_TrackPosition.state.toString.replaceAll("[\\D]", "")) * 10
if(null === actions) {
logInfo("actions", "Actions not found, check thing ID")
return
}
switch playMode {
case "Nothing playing": {
// when idle, '1' tells Nuvo to display 'idle' on the keypad
actions.sendNuvoCommand("S3DISPINFO,0,0,1")
}
case "Playing": {
// when playback starts or resumes, '2' tells Nuvo to display 'playing' on the keypad
// trackPosition does not need to be updated continuously, Nuvo will automatically count up the elapsed time displayed on the keypad
actions.sendNuvoCommand("S3DISPINFO," + trackLength.toString() + "," + trackPosition.toString() + ",2")
}
case "Paused": {
// when playback is paused, '3' tells Nuvo to display 'paused' on the keypad and stop counting up the elapsed time
// trackPosition should indicate the time elapsed of the track when playback was paused
actions.sendNuvoCommand("S3DISPINFO," + trackLength.toString() + "," + trackPosition.toString() + ",3")
}
}
end
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.nuvo</artifactId>
<name>openHAB Add-ons :: Bundles :: Nuvo Binding</name>
</project>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.nuvo-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-nuvo" description="Nuvo Whole House Audio Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-serial</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.nuvo/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nuvo.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link INuvoThingActions} defines the interface for all thing actions supported by the binding.
* These methods, parameters, and return types are explained in {@link NuvoThingActions}.
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public interface INuvoThingActions {
void sendNuvoCommand(String rawCommand);
}

View File

@@ -0,0 +1,85 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nuvo.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link NuvoBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class NuvoBindingConstants {
public static final String BINDING_ID = "nuvo";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_AMP = new ThingTypeUID(BINDING_ID, "amplifier");
// List of all Channel types
// system
public static final String CHANNEL_TYPE_ALLOFF = "alloff";
public static final String CHANNEL_TYPE_ALLMUTE = "allmute";
public static final String CHANNEL_TYPE_PAGE = "page";
public static final String CHANNEL_TYPE_SENDCMD = "sendcmd";
// zone
public static final String CHANNEL_TYPE_POWER = "power";
public static final String CHANNEL_TYPE_SOURCE = "source";
public static final String CHANNEL_TYPE_VOLUME = "volume";
public static final String CHANNEL_TYPE_MUTE = "mute";
public static final String CHANNEL_TYPE_CONTROL = "control";
public static final String CHANNEL_TYPE_TREBLE = "treble";
public static final String CHANNEL_TYPE_BASS = "bass";
public static final String CHANNEL_TYPE_BALANCE = "balance";
public static final String CHANNEL_TYPE_LOUDNESS = "loudness";
public static final String CHANNEL_TYPE_DND = "dnd";
public static final String CHANNEL_TYPE_LOCK = "lock";
public static final String CHANNEL_TYPE_PARTY = "party";
// source
public static final String CHANNEL_DISPLAY_LINE = "display_line";
public static final String CHANNEL_DISPLAY_LINE1 = "display_line1";
public static final String CHANNEL_DISPLAY_LINE2 = "display_line2";
public static final String CHANNEL_DISPLAY_LINE3 = "display_line3";
public static final String CHANNEL_DISPLAY_LINE4 = "display_line4";
public static final String CHANNEL_PLAY_MODE = "play_mode";
public static final String CHANNEL_TRACK_LENGTH = "track_length";
public static final String CHANNEL_TRACK_POSITION = "track_position";
public static final String CHANNEL_BUTTON_PRESS = "button_press";
// Message types
public static final String TYPE_VERSION = "version";
public static final String TYPE_ALLOFF = "alloff";
public static final String TYPE_ALLMUTE = "allmute";
public static final String TYPE_PAGE = "page";
public static final String TYPE_SOURCE_UPDATE = "source_update";
public static final String TYPE_ZONE_UPDATE = "zone_update";
public static final String TYPE_ZONE_BUTTON = "zone_button";
public static final String TYPE_ZONE_CONFIG = "zone_config";
// misc
public static final String ON = "ON";
public static final String OFF = "OFF";
public static final String ONE = "1";
public static final String ZERO = "0";
public static final String BLANK = "";
public static final String DISPLINE = "DISPLINE";
public static final String DISPINFO = "DISPINFO,"; // yes comma here
public static final String NAME_QUOTE = "NAME\"";
public static final String MUTE = "MUTE";
public static final String VOL = "VOL";
}

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nuvo.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link NuvoException} class is used for any exception thrown by the binding
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class NuvoException extends Exception {
private static final long serialVersionUID = 1L;
public NuvoException() {
}
public NuvoException(String message, Throwable t) {
super(message, t);
}
public NuvoException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,71 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nuvo.internal;
import static org.openhab.binding.nuvo.internal.NuvoBindingConstants.*;
import java.util.Collections;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nuvo.internal.handler.NuvoHandler;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link NuvoHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.nuvo", service = ThingHandlerFactory.class)
public class NuvoHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_AMP);
private final SerialPortManager serialPortManager;
private final NuvoStateDescriptionOptionProvider stateDescriptionProvider;
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Activate
public NuvoHandlerFactory(final @Reference NuvoStateDescriptionOptionProvider provider,
final @Reference SerialPortManager serialPortManager) {
this.stateDescriptionProvider = provider;
this.serialPortManager = serialPortManager;
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
return new NuvoHandler(thing, stateDescriptionProvider, serialPortManager);
}
return null;
}
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nuvo.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* Dynamic provider of state options while leaving other state description fields as original.
*
* @author Michael Lobstein - Initial contribution
*/
@Component(service = { DynamicStateDescriptionProvider.class, NuvoStateDescriptionOptionProvider.class })
@NonNullByDefault
public class NuvoStateDescriptionOptionProvider extends BaseDynamicStateDescriptionProvider {
@Reference
protected void setChannelTypeI18nLocalizationService(
final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
}
protected void unsetChannelTypeI18nLocalizationService(
final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.channelTypeI18nLocalizationService = null;
}
}

View File

@@ -0,0 +1,88 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nuvo.internal;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nuvo.internal.handler.NuvoHandler;
import org.openhab.core.automation.annotation.ActionInput;
import org.openhab.core.automation.annotation.RuleAction;
import org.openhab.core.thing.binding.ThingActions;
import org.openhab.core.thing.binding.ThingActionsScope;
import org.openhab.core.thing.binding.ThingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Some automation actions to be used with a {@link NuvoThingActions}
*
* @author Michael Lobstein - initial contribution
*
*/
@ThingActionsScope(name = "nuvo")
@NonNullByDefault
public class NuvoThingActions implements ThingActions, INuvoThingActions {
private final Logger logger = LoggerFactory.getLogger(NuvoThingActions.class);
private @Nullable NuvoHandler handler;
@RuleAction(label = "sendNuvoCommand", description = "Action that sends raw command to the amplifer")
public void sendNuvoCommand(@ActionInput(name = "sendNuvoCommand") String rawCommand) {
NuvoHandler localHandler = handler;
if (localHandler != null) {
localHandler.handleRawCommand(rawCommand);
logger.debug("sendNuvoCommand called with raw command: {}", rawCommand);
} else {
logger.warn("unable to send command, NuvoHandler was null");
}
}
/** Static alias to support the old DSL rules engine and make the action available there. */
public static void sendNuvoCommand(@Nullable ThingActions actions, String rawCommand)
throws IllegalArgumentException {
invokeMethodOf(actions).sendNuvoCommand(rawCommand);
}
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
this.handler = (NuvoHandler) handler;
}
@Override
public @Nullable ThingHandler getThingHandler() {
return this.handler;
}
private static INuvoThingActions invokeMethodOf(@Nullable ThingActions actions) {
if (actions == null) {
throw new IllegalArgumentException("actions cannot be null");
}
if (actions.getClass().getName().equals(NuvoThingActions.class.getName())) {
if (actions instanceof NuvoThingActions) {
return (INuvoThingActions) actions;
} else {
return (INuvoThingActions) Proxy.newProxyInstance(INuvoThingActions.class.getClassLoader(),
new Class[] { INuvoThingActions.class }, (Object proxy, Method method, Object[] args) -> {
Method m = actions.getClass().getDeclaredMethod(method.getName(),
method.getParameterTypes());
return m.invoke(actions, args);
});
}
}
throw new IllegalArgumentException("Actions is not an instance of NuvoThingActions");
}
}

View File

@@ -0,0 +1,72 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nuvo.internal.communication;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Represents the different kinds of commands
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public enum NuvoCommand {
GET_CONTROLLER_VERSION("VER"),
ALLMUTE_ON("MUTE1"),
ALLMUTE_OFF("MUTE0"),
ALLOFF("ALLOFF"),
PAGE_ON("PAGE1"),
PAGE_OFF("PAGE0"),
CFGTIME("CFGTIME"),
STATUS("STATUS"),
EQ_QUERY("EQ?"),
DISPINFO("DISPINFO"),
DISPLINE("DISPLINE"),
DISPLINE1("DISPLINE1"),
DISPLINE2("DISPLINE2"),
DISPLINE3("DISPLINE3"),
DISPLINE4("DISPLINE4"),
NAME("NAME"),
ON("ON"),
OFF("OFF"),
SOURCE("SRC"),
VOLUME("VOL"),
MUTE_ON("MUTEON"),
MUTE_OFF("MUTEOFF"),
TREBLE("TREB"),
BASS("BASS"),
BALANCE("BAL"),
LOUDNESS("LOUDCMP"),
PLAYPAUSE("PLAYPAUSE"),
PREV("PREV"),
NEXT("NEXT"),
DND_ON("DNDON"),
DND_OFF("DNDOFF"),
PARTY_ON("PARTY1"),
PARTY_OFF("PARTY0");
private final String value;
NuvoCommand(String value) {
this.value = value;
}
/**
* Get the command name
*
* @return the command name
*/
public String getValue() {
return value;
}
}

View File

@@ -0,0 +1,376 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nuvo.internal.communication;
import static org.openhab.binding.nuvo.internal.NuvoBindingConstants.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nuvo.internal.NuvoException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Abstract class for communicating with the Nuvo device
*
* @author Laurent Garnier - Initial contribution
* @author Michael Lobstein - Adapted for the Nuvo binding
*/
@NonNullByDefault
public abstract class NuvoConnector {
private static final String COMMAND_OK = "#OK";
private static final String BEGIN_CMD = "*";
private static final String END_CMD = "\r";
private static final String QUERY = "?";
private static final String VER_STR = "#VER\"NV-";
private static final String ALL_OFF = "#ALLOFF";
private static final String MUTE = "#MUTE";
private static final String PAGE = "#PAGE";
private static final byte[] WAKE_STR = "\r".getBytes(StandardCharsets.US_ASCII);
private static final Pattern SRC_PATTERN = Pattern.compile("^#S(\\d{1})(.*)$");
private static final Pattern ZONE_PATTERN = Pattern.compile("^#Z(\\d{1,2}),(.*)$");
private static final Pattern ZONE_BUTTON_PATTERN = Pattern.compile("^#Z(\\d{1,2})S(\\d{1})(.*)$");
private static final Pattern ZONE_CFG_PATTERN = Pattern.compile("^#ZCFG(\\d{1,2}),(.*)$");
private final Logger logger = LoggerFactory.getLogger(NuvoConnector.class);
protected static final String COMMAND_ERROR = "#?";
/** The output stream */
protected @Nullable OutputStream dataOut;
/** The input stream */
protected @Nullable InputStream dataIn;
/** true if the connection is established, false if not */
private boolean connected;
private @Nullable Thread readerThread;
private List<NuvoMessageEventListener> listeners = new ArrayList<>();
private boolean isEssentia = true;
/**
* Get whether the connection is established or not
*
* @return true if the connection is established
*/
public boolean isConnected() {
return connected;
}
/**
* Set whether the connection is established or not
*
* @param connected true if the connection is established
*/
protected void setConnected(boolean connected) {
this.connected = connected;
}
/**
* Tell the connector if the device is an Essentia G or not
*
* @param true if the device is an Essentia G
*/
public void setEssentia(boolean isEssentia) {
this.isEssentia = isEssentia;
}
/**
* Set the thread that handles the feedback messages
*
* @param readerThread the thread
*/
protected void setReaderThread(Thread readerThread) {
this.readerThread = readerThread;
}
/**
* Open the connection with the Nuvo device
*
* @throws NuvoException - In case of any problem
*/
public abstract void open() throws NuvoException;
/**
* Close the connection with the Nuvo device
*/
public abstract void close();
/**
* Stop the thread that handles the feedback messages and close the opened input and output streams
*/
protected void cleanup() {
Thread readerThread = this.readerThread;
OutputStream dataOut = this.dataOut;
if (dataOut != null) {
try {
dataOut.close();
} catch (IOException e) {
logger.debug("Error closing dataOut: {}", e.getMessage());
}
this.dataOut = null;
}
InputStream dataIn = this.dataIn;
if (dataIn != null) {
try {
dataIn.close();
} catch (IOException e) {
logger.debug("Error closing dataIn: {}", e.getMessage());
}
this.dataIn = null;
}
if (readerThread != null) {
readerThread.interrupt();
this.readerThread = null;
try {
readerThread.join(3000);
} catch (InterruptedException e) {
logger.warn("Error joining readerThread: {}", e.getMessage());
}
}
}
/**
* Reads some number of bytes from the input stream and stores them into the buffer array b. The number of bytes
* actually read is returned as an integer.
*
* @param dataBuffer the buffer into which the data is read.
*
* @return the total number of bytes read into the buffer, or -1 if there is no more data because the end of the
* stream has been reached.
*
* @throws NuvoException - If the input stream is null, if the first byte cannot be read for any reason
* other than the end of the file, if the input stream has been closed, or if some other I/O error
* occurs.
*/
protected int readInput(byte[] dataBuffer) throws NuvoException {
InputStream dataIn = this.dataIn;
if (dataIn == null) {
throw new NuvoException("readInput failed: input stream is null");
}
try {
return dataIn.read(dataBuffer);
} catch (IOException e) {
throw new NuvoException("readInput failed: " + e.getMessage(), e);
}
}
/**
* Request the Nuvo controller to execute an inquiry command
*
* @param zone the zone for which the command is to be run
* @param cmd the command to execute
*
* @throws NuvoException - In case of any problem
*/
public void sendQuery(NuvoEnum zone, NuvoCommand cmd) throws NuvoException {
sendCommand(zone.getId() + cmd.getValue() + QUERY);
}
/**
* Request the Nuvo controller to execute a command for a zone that takes no arguments (ie power on, power off,
* etc.)
*
* @param zone the zone for which the command is to be run
* @param cmd the command to execute
*
* @throws NuvoException - In case of any problem
*/
public void sendCommand(NuvoEnum zone, NuvoCommand cmd) throws NuvoException {
sendCommand(zone.getId() + cmd.getValue());
}
/**
* Request the Nuvo controller to execute a command for a zone and pass in a value
*
* @param zone the zone for which the command is to be run
* @param cmd the command to execute
* @param value the string value to consider for volume, source, etc.
*
* @throws NuvoException - In case of any problem
*/
public void sendCommand(NuvoEnum zone, NuvoCommand cmd, @Nullable String value) throws NuvoException {
sendCommand(zone.getId() + cmd.getValue() + value);
}
/**
* Request the Nuvo controller to execute a configuration command for a zone and pass in a value
*
* @param zone the zone for which the command is to be run
* @param cmd the command to execute
* @param value the string value to consider for bass, treble, balance, etc.
*
* @throws NuvoException - In case of any problem
*/
public void sendCfgCommand(NuvoEnum zone, NuvoCommand cmd, @Nullable String value) throws NuvoException {
sendCommand(zone.getConfigId() + cmd.getValue() + value);
}
/**
* Request the Nuvo controller to execute a system command the does not specify a zone or value
*
* @param cmd the command to execute
*
* @throws NuvoException - In case of any problem
*/
public void sendCommand(NuvoCommand cmd) throws NuvoException {
sendCommand(cmd.getValue());
}
/**
* Request the Nuvo controller to execute a raw command string
*
* @param command the command string to run
*
* @throws NuvoException - In case of any problem
*/
public void sendCommand(@Nullable String command) throws NuvoException {
String messageStr = BEGIN_CMD + command + END_CMD;
logger.debug("sending command: {}", messageStr);
OutputStream dataOut = this.dataOut;
if (dataOut == null) {
throw new NuvoException("Send command \"" + messageStr + "\" failed: output stream is null");
}
try {
// Essentia G needs time to wake up when in standby mode
// I don't want to track that in the binding, so just do this always
if (this.isEssentia) {
dataOut.write(WAKE_STR);
dataOut.flush();
}
dataOut.write(messageStr.getBytes(StandardCharsets.US_ASCII));
dataOut.flush();
} catch (IOException e) {
throw new NuvoException("Send command \"" + command + "\" failed: " + e.getMessage(), e);
}
}
/**
* Add a listener to the list of listeners to be notified with events
*
* @param listener the listener
*/
public void addEventListener(NuvoMessageEventListener listener) {
listeners.add(listener);
}
/**
* Remove a listener from the list of listeners to be notified with events
*
* @param listener the listener
*/
public void removeEventListener(NuvoMessageEventListener listener) {
listeners.remove(listener);
}
/**
* Analyze an incoming message and dispatch corresponding (type, key, value) to the event listeners
*
* @param incomingMessage the received message
*/
public void handleIncomingMessage(byte[] incomingMessage) {
String message = new String(incomingMessage, StandardCharsets.US_ASCII).trim();
logger.debug("handleIncomingMessage: {}", message);
if (COMMAND_ERROR.equals(message) || COMMAND_OK.equals(message)) {
// ignore
return;
}
if (message.contains(VER_STR)) {
// example: #VER"NV-E6G FWv2.66 HWv0"
// split on " and return the version number
dispatchKeyValue(TYPE_VERSION, "", message.split("\"")[1]);
return;
}
if (message.equals(ALL_OFF)) {
dispatchKeyValue(TYPE_ALLOFF, BLANK, BLANK);
return;
}
if (message.contains(MUTE)) {
dispatchKeyValue(TYPE_ALLMUTE, BLANK, message.substring(message.length() - 1));
return;
}
if (message.contains(PAGE)) {
dispatchKeyValue(TYPE_PAGE, BLANK, message.substring(message.length() - 1));
return;
}
// Amp controller send a source update ie: #S2DISPINFO,DUR3380,POS3090,STATUS2
// or #S2DISPLINE1,"1 of 17"
Matcher matcher = SRC_PATTERN.matcher(message);
if (matcher.find()) {
// pull out the source id and the remainder of the message
dispatchKeyValue(TYPE_SOURCE_UPDATE, matcher.group(1), matcher.group(2));
return;
}
// Amp controller send a zone update ie: #Z11,ON,SRC3,VOL63,DND0,LOCK0
matcher = ZONE_PATTERN.matcher(message);
if (matcher.find()) {
// pull out the zone id and the remainder of the message
dispatchKeyValue(TYPE_ZONE_UPDATE, matcher.group(1), matcher.group(2));
return;
}
// Amp controller send a zone button press event ie: #Z11S3PLAYPAUSE
matcher = ZONE_BUTTON_PATTERN.matcher(message);
if (matcher.find()) {
// pull out the source id and the remainder of the message, ignore the zone id
dispatchKeyValue(TYPE_ZONE_BUTTON, matcher.group(2), matcher.group(3));
return;
}
// Amp controller send a zone configuration response ie: #ZCFG1,BASS1,TREB-2,BALR2,LOUDCMP1
matcher = ZONE_CFG_PATTERN.matcher(message);
if (matcher.find()) {
// pull out the zone id and the remainder of the message
dispatchKeyValue(TYPE_ZONE_CONFIG, matcher.group(1), matcher.group(2));
return;
}
logger.debug("unhandled message: {}", message);
}
/**
* Dispatch an event (type, key, value) to the event listeners
*
* @param type the type
* @param key the key
* @param value the value
*/
private void dispatchKeyValue(String type, String key, String value) {
NuvoMessageEvent event = new NuvoMessageEvent(this, type, key, value);
listeners.forEach(l -> l.onNewMessageEvent(event));
}
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nuvo.internal.communication;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.nuvo.internal.NuvoException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Class to create a default NuvoDefaultConnector before initialization is complete.
*
* @author Laurent Garnier - Initial contribution
* @author Michael Lobstein - Adapted for the Nuvo binding
*/
@NonNullByDefault
public class NuvoDefaultConnector extends NuvoConnector {
private final Logger logger = LoggerFactory.getLogger(NuvoDefaultConnector.class);
@Override
public void open() throws NuvoException {
logger.warn("Nuvo binding incorrectly configured. Please configure for Serial or IP over serial connection");
setConnected(false);
}
@Override
public void close() {
setConnected(false);
}
}

View File

@@ -0,0 +1,85 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nuvo.internal.communication;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Represents the different internal zone and source IDs of the Nuvo Whole House Amplifier
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public enum NuvoEnum {
SYSTEM("SYSTEM", "SYSTEM"),
ZONE1("Z1", "ZCFG1"),
ZONE2("Z2", "ZCFG2"),
ZONE3("Z3", "ZCFG3"),
ZONE4("Z4", "ZCFG4"),
ZONE5("Z5", "ZCFG5"),
ZONE6("Z6", "ZCFG6"),
ZONE7("Z7", "ZCFG7"),
ZONE8("Z8", "ZCFG8"),
ZONE9("Z9", "ZCFG9"),
ZONE10("Z10", "ZCFG10"),
ZONE11("Z11", "ZCFG11"),
ZONE12("Z12", "ZCFG12"),
ZONE13("Z13", "ZCFG13"),
ZONE14("Z14", "ZCFG14"),
ZONE15("Z15", "ZCFG15"),
ZONE16("Z16", "ZCFG16"),
ZONE17("Z17", "ZCFG17"),
ZONE18("Z18", "ZCFG18"),
ZONE19("Z19", "ZCFG19"),
ZONE20("Z20", "ZCFG20"),
SOURCE1("S1", "SCFG1"),
SOURCE2("S2", "SCFG2"),
SOURCE3("S3", "SCFG3"),
SOURCE4("S4", "SCFG4"),
SOURCE5("S5", "SCFG5"),
SOURCE6("S6", "SCFG6");
private final String id;
private final String cfgId;
// make a list of all valid source ids
public static final List<String> VALID_SOURCES = Arrays.stream(values()).map(NuvoEnum::name)
.filter(s -> s.contains("SOURCE")).collect(Collectors.toList());
NuvoEnum(String id, String cfgId) {
this.id = id;
this.cfgId = cfgId;
}
/**
* Get the id
*
* @return the id
*/
public String getId() {
return id;
}
/**
* Get the config id
*
* @return the config id
*/
public String getConfigId() {
return cfgId;
}
}

View File

@@ -0,0 +1,127 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nuvo.internal.communication;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.net.SocketTimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nuvo.internal.NuvoException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Class for communicating with the Nuvo device through a serial over IP connection
*
* @author Laurent Garnier - Initial contribution
* @author Michael Lobstein - Adapted for the Nuvo binding
*/
@NonNullByDefault
public class NuvoIpConnector extends NuvoConnector {
private final Logger logger = LoggerFactory.getLogger(NuvoIpConnector.class);
private @Nullable final String address;
private final int port;
private final String uid;
private @Nullable Socket clientSocket;
/**
* Constructor
*
* @param address the IP address of the serial over ip adapter
* @param port the TCP port to be used
* @param uid the thing uid string
*/
public NuvoIpConnector(@Nullable String address, int port, String uid) {
this.address = address;
this.port = port;
this.uid = uid;
}
@Override
public synchronized void open() throws NuvoException {
logger.debug("Opening IP connection on IP {} port {}", this.address, this.port);
try {
Socket clientSocket = new Socket(this.address, this.port);
clientSocket.setSoTimeout(100);
dataOut = new DataOutputStream(clientSocket.getOutputStream());
dataIn = new DataInputStream(clientSocket.getInputStream());
Thread thread = new NuvoReaderThread(this, this.uid, this.address + "." + this.port);
setReaderThread(thread);
thread.start();
this.clientSocket = clientSocket;
setConnected(true);
logger.debug("IP connection opened");
} catch (IOException | SecurityException | IllegalArgumentException e) {
setConnected(false);
throw new NuvoException("Opening IP connection failed: " + e.getMessage(), e);
}
}
@Override
public synchronized void close() {
logger.debug("Closing IP connection");
super.cleanup();
Socket clientSocket = this.clientSocket;
if (clientSocket != null) {
try {
clientSocket.close();
} catch (IOException e) {
}
this.clientSocket = null;
}
setConnected(false);
logger.debug("IP connection closed");
}
/**
* Reads some number of bytes from the input stream and stores them into the buffer array b. The number of bytes
* actually read is returned as an integer.
* In case of socket timeout, the returned value is 0.
*
* @param dataBuffer the buffer into which the data is read.
*
* @return the total number of bytes read into the buffer, or -1 if there is no more data because the end of the
* stream has been reached.
*
* @throws NuvoException - If the input stream is null, if the first byte cannot be read for any reason
* other than the end of the file, if the input stream has been closed, or if some other I/O error
* occurs.
*/
@Override
protected int readInput(byte[] dataBuffer) throws NuvoException {
InputStream dataIn = this.dataIn;
if (dataIn == null) {
throw new NuvoException("readInput failed: input stream is null");
}
try {
return dataIn.read(dataBuffer);
} catch (SocketTimeoutException e) {
return 0;
} catch (IOException e) {
throw new NuvoException("readInput failed: " + e.getMessage(), e);
}
}
}

View File

@@ -0,0 +1,49 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nuvo.internal.communication;
import java.util.EventObject;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* NuvoMessageEvent event used to notify changes coming from messages received from the Nuvo device
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class NuvoMessageEvent extends EventObject {
private static final long serialVersionUID = 1L;
private final String type;
private final String key;
private final String value;
public NuvoMessageEvent(Object source, String type, String key, String value) {
super(source);
this.type = type;
this.key = key;
this.value = value;
}
public String getType() {
return type;
}
public String getKey() {
return key;
}
public String getValue() {
return value;
}
}

View File

@@ -0,0 +1,33 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nuvo.internal.communication;
import java.util.EventListener;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Nuvo Event Listener interface. Handles incoming Nuvo message events
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public interface NuvoMessageEventListener extends EventListener {
/**
* Event handler method for incoming Nuvo message events
*
* @param event the NuvoMessageEvent object
*/
public void onNewMessageEvent(NuvoMessageEvent event);
}

View File

@@ -0,0 +1,89 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nuvo.internal.communication;
import java.util.Arrays;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.nuvo.internal.NuvoException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A class that reads messages from the Nuvo device in a dedicated thread
*
* @author Laurent Garnier - Initial contribution
* @author Michael Lobstein - Adapted for the Nuvo binding
*/
@NonNullByDefault
public class NuvoReaderThread extends Thread {
private final Logger logger = LoggerFactory.getLogger(NuvoReaderThread.class);
private static final int READ_BUFFER_SIZE = 16;
private static final int SIZE = 256;
private static final char TERM_CHAR = '\r';
private NuvoConnector connector;
/**
* Constructor
*
* @param connector the object that should handle the received message
* @param uid the thing uid string
* @param connectionId a string that uniquely identifies the particular connection
*/
public NuvoReaderThread(NuvoConnector connector, String uid, String connectionId) {
super("OH-binding-" + uid + "-" + connectionId);
this.connector = connector;
setDaemon(true);
}
@Override
public void run() {
logger.debug("Data listener started");
byte[] readDataBuffer = new byte[READ_BUFFER_SIZE];
byte[] dataBuffer = new byte[SIZE];
int index = 0;
try {
while (!Thread.interrupted()) {
int len = connector.readInput(readDataBuffer);
if (len > 0) {
for (int i = 0; i < len; i++) {
if (index < SIZE) {
dataBuffer[index++] = readDataBuffer[i];
}
if (readDataBuffer[i] == TERM_CHAR) {
if (index >= SIZE) {
dataBuffer[index - 1] = (byte) TERM_CHAR;
}
byte[] msg = Arrays.copyOf(dataBuffer, index);
connector.handleIncomingMessage(msg);
index = 0;
}
}
}
}
} catch (NuvoException e) {
logger.debug("Reading failed: {}", e.getMessage(), e);
connector.handleIncomingMessage(NuvoConnector.COMMAND_ERROR.getBytes());
}
logger.debug("Data listener stopped");
}
}

View File

@@ -0,0 +1,133 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nuvo.internal.communication;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nuvo.internal.NuvoException;
import org.openhab.core.io.transport.serial.PortInUseException;
import org.openhab.core.io.transport.serial.SerialPort;
import org.openhab.core.io.transport.serial.SerialPortIdentifier;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Class for communicating with the Nuvo device through a serial connection
*
* @author Laurent Garnier - Initial contribution
* @author Michael Lobstein - Adapted for the Nuvo binding
*/
@NonNullByDefault
public class NuvoSerialConnector extends NuvoConnector {
private final Logger logger = LoggerFactory.getLogger(NuvoSerialConnector.class);
private final String serialPortName;
private final SerialPortManager serialPortManager;
private final String uid;
private @Nullable SerialPort serialPort;
/**
* Constructor
*
* @param serialPortManager the serial port manager
* @param serialPortName the serial port name to be used
* @param uid the thing uid string
*/
public NuvoSerialConnector(SerialPortManager serialPortManager, String serialPortName, String uid) {
this.serialPortManager = serialPortManager;
this.serialPortName = serialPortName;
this.uid = uid;
}
@Override
public synchronized void open() throws NuvoException {
logger.debug("Opening serial connection on port {}", serialPortName);
try {
SerialPortIdentifier portIdentifier = serialPortManager.getIdentifier(serialPortName);
if (portIdentifier == null) {
setConnected(false);
logger.warn("Opening serial connection failed: No Such Port: {}", serialPortName);
throw new NuvoException("Opening serial connection failed: No Such Port");
}
SerialPort commPort = portIdentifier.open(this.getClass().getName(), 2000);
commPort.setSerialPortParams(57600, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);
commPort.enableReceiveThreshold(1);
commPort.enableReceiveTimeout(100);
commPort.setFlowControlMode(SerialPort.FLOWCONTROL_NONE);
InputStream dataIn = commPort.getInputStream();
OutputStream dataOut = commPort.getOutputStream();
if (dataOut != null) {
dataOut.flush();
}
if (dataIn != null && dataIn.markSupported()) {
try {
dataIn.reset();
} catch (IOException e) {
}
}
Thread thread = new NuvoReaderThread(this, this.uid, this.serialPortName);
setReaderThread(thread);
thread.start();
this.serialPort = commPort;
this.dataIn = dataIn;
this.dataOut = dataOut;
setConnected(true);
logger.debug("Serial connection opened");
} catch (PortInUseException e) {
setConnected(false);
throw new NuvoException("Opening serial connection failed: Port in Use Exception", e);
} catch (UnsupportedCommOperationException e) {
setConnected(false);
throw new NuvoException("Opening serial connection failed: Unsupported Comm Operation Exception", e);
} catch (UnsupportedEncodingException e) {
setConnected(false);
throw new NuvoException("Opening serial connection failed: Unsupported Encoding Exception", e);
} catch (IOException e) {
setConnected(false);
throw new NuvoException("Opening serial connection failed: IO Exception", e);
}
}
@Override
public synchronized void close() {
logger.debug("Closing serial connection");
SerialPort serialPort = this.serialPort;
if (serialPort != null) {
serialPort.removeEventListener();
}
super.cleanup();
if (serialPort != null) {
serialPort.close();
this.serialPort = null;
}
setConnected(false);
logger.debug("Serial connection closed");
}
}

View File

@@ -0,0 +1,82 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nuvo.internal.communication;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Provides mapping of various Nuvo status codes to plain language meanings
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class NuvoStatusCodes {
private static final String L = "L";
private static final String C = "C";
private static final String R = "R";
private static final String DASH = "-";
private static final String ZERO = "0";
// map to lookup play mode
public static final Map<String, String> PLAY_MODE = new HashMap<>();
static {
PLAY_MODE.put("0", "Normal");
PLAY_MODE.put("1", "Idle");
PLAY_MODE.put("2", "Playing");
PLAY_MODE.put("3", "Paused");
PLAY_MODE.put("4", "Fast Forward");
PLAY_MODE.put("5", "Rewind");
PLAY_MODE.put("6", "Play Shuffle");
PLAY_MODE.put("7", "Play Repeat");
PLAY_MODE.put("8", "Play Shuffle Repeat");
PLAY_MODE.put("9", "unknown-9");
PLAY_MODE.put("10", "unknown-10");
PLAY_MODE.put("11", "Radio"); // undocumented
PLAY_MODE.put("12", "unknown-12");
}
/*
* This looks broken because the controller is seriously broken...
* On the keypad when adjusting the balance to "Left 18", the serial data reports R18 ¯\_(ツ)_/¯
* So on top of the weird translation, the value needs to be reversed by the binding
* to ensure that it will match what is displayed on the keypad.
* For display purposes we want -18 to be full left, 0 = center, and +18 to be full right
*/
public static String getBalanceFromStr(String value) {
// example L2; return 2 | C; return 0 | R10; return -10
if (value.substring(0, 1).equals(L)) {
return (value.substring(1));
} else if (value.equals(C)) {
return ZERO;
} else if (value.substring(0, 1).equals(R)) {
return (DASH + value.substring(1));
}
return ZERO;
}
// see above comment
public static String getBalanceFromInt(Integer value) {
if (value < 0) {
return (L + Math.abs(value));
} else if (value == 0) {
return C;
} else if (value > 0) {
return (R + value);
}
return C;
}
}

View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nuvo.internal.configuration;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link NuvoThingConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class NuvoThingConfiguration {
public @Nullable String serialPort;
public @Nullable String host;
public @Nullable Integer port;
public @Nullable Integer numZones;
public boolean clockSync;
}

View File

@@ -0,0 +1,793 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.nuvo.internal.handler;
import static org.openhab.binding.nuvo.internal.NuvoBindingConstants.*;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javax.measure.Unit;
import javax.measure.quantity.Time;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.nuvo.internal.NuvoException;
import org.openhab.binding.nuvo.internal.NuvoStateDescriptionOptionProvider;
import org.openhab.binding.nuvo.internal.NuvoThingActions;
import org.openhab.binding.nuvo.internal.communication.NuvoCommand;
import org.openhab.binding.nuvo.internal.communication.NuvoConnector;
import org.openhab.binding.nuvo.internal.communication.NuvoDefaultConnector;
import org.openhab.binding.nuvo.internal.communication.NuvoEnum;
import org.openhab.binding.nuvo.internal.communication.NuvoIpConnector;
import org.openhab.binding.nuvo.internal.communication.NuvoMessageEvent;
import org.openhab.binding.nuvo.internal.communication.NuvoMessageEventListener;
import org.openhab.binding.nuvo.internal.communication.NuvoSerialConnector;
import org.openhab.binding.nuvo.internal.communication.NuvoStatusCodes;
import org.openhab.binding.nuvo.internal.configuration.NuvoThingConfiguration;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.NextPreviousType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.PlayPauseType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.SmartHomeUnits;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.StateOption;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link NuvoHandler} is responsible for handling commands, which are sent to one of the channels.
*
* Based on the Rotel binding by Laurent Garnier
*
* @author Michael Lobstein - Initial contribution
*/
@NonNullByDefault
public class NuvoHandler extends BaseThingHandler implements NuvoMessageEventListener {
private static final long RECON_POLLING_INTERVAL_SEC = 60;
private static final long POLLING_INTERVAL_SEC = 30;
private static final long CLOCK_SYNC_INTERVAL_SEC = 3600;
private static final long INITIAL_POLLING_DELAY_SEC = 30;
private static final long INITIAL_CLOCK_SYNC_DELAY_SEC = 10;
// spec says wait 50ms, min is 100
private static final long SLEEP_BETWEEN_CMD_MS = 100;
private static final Unit<Time> API_SECOND_UNIT = SmartHomeUnits.SECOND;
private static final String ZONE = "ZONE";
private static final String SOURCE = "SOURCE";
private static final String CHANNEL_DELIMIT = "#";
private static final String UNDEF = "UNDEF";
private static final String GC_STR = "NV-IG8";
private static final int MAX_ZONES = 20;
private static final int MAX_SRC = 6;
private static final int MIN_VOLUME = 0;
private static final int MAX_VOLUME = 79;
private static final int MIN_EQ = -18;
private static final int MAX_EQ = 18;
private static final Pattern ZONE_PATTERN = Pattern
.compile("^ON,SRC(\\d{1}),(MUTE|VOL\\d{1,2}),DND([0-1]),LOCK([0-1])$");
private static final Pattern DISP_PATTERN = Pattern.compile("^DISPLINE(\\d{1}),\"(.*)\"$");
private static final Pattern DISP_INFO_PATTERN = Pattern
.compile("^DISPINFO,DUR(\\d{1,6}),POS(\\d{1,6}),STATUS(\\d{1,2})$");
private static final Pattern ZONE_CFG_PATTERN = Pattern.compile("^BASS(.*),TREB(.*),BAL(.*),LOUDCMP([0-1])$");
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy,MM,dd,HH,mm");
private final Logger logger = LoggerFactory.getLogger(NuvoHandler.class);
private final NuvoStateDescriptionOptionProvider stateDescriptionProvider;
private final SerialPortManager serialPortManager;
private @Nullable ScheduledFuture<?> reconnectJob;
private @Nullable ScheduledFuture<?> pollingJob;
private @Nullable ScheduledFuture<?> clockSyncJob;
private NuvoConnector connector = new NuvoDefaultConnector();
private long lastEventReceived = System.currentTimeMillis();
private int numZones = 1;
private String versionString = BLANK;
private boolean isGConcerto = false;
private Object sequenceLock = new Object();
Set<Integer> activeZones = new HashSet<>(1);
// A state option list for the source labels
List<StateOption> sourceLabels = new ArrayList<>();
/**
* Constructor
*/
public NuvoHandler(Thing thing, NuvoStateDescriptionOptionProvider stateDescriptionProvider,
SerialPortManager serialPortManager) {
super(thing);
this.stateDescriptionProvider = stateDescriptionProvider;
this.serialPortManager = serialPortManager;
}
@Override
public void initialize() {
final String uid = this.getThing().getUID().getAsString();
NuvoThingConfiguration config = getConfigAs(NuvoThingConfiguration.class);
final String serialPort = config.serialPort;
final String host = config.host;
final Integer port = config.port;
final Integer numZones = config.numZones;
// Check configuration settings
String configError = null;
if ((serialPort == null || serialPort.isEmpty()) && (host == null || host.isEmpty())) {
configError = "undefined serialPort and host configuration settings; please set one of them";
} else if (serialPort != null && (host == null || host.isEmpty())) {
if (serialPort.toLowerCase().startsWith("rfc2217")) {
configError = "use host and port configuration settings for a serial over IP connection";
}
} else {
if (port == null) {
configError = "undefined port configuration setting";
} else if (port <= 0) {
configError = "invalid port configuration setting";
}
}
if (configError != null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, configError);
return;
}
if (serialPort != null) {
connector = new NuvoSerialConnector(serialPortManager, serialPort, uid);
} else if (port != null) {
connector = new NuvoIpConnector(host, port, uid);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Either Serial port or Host & Port must be specifed");
return;
}
if (numZones != null) {
this.numZones = numZones;
}
activeZones = IntStream.range((1), (this.numZones + 1)).boxed().collect(Collectors.toSet());
// remove the channels for the zones we are not using
if (this.numZones < MAX_ZONES) {
List<Channel> channels = new ArrayList<>(this.getThing().getChannels());
List<Integer> zonesToRemove = IntStream.range((this.numZones + 1), (MAX_ZONES + 1)).boxed()
.collect(Collectors.toList());
zonesToRemove.forEach(zone -> channels.removeIf(c -> (c.getUID().getId().contains("zone" + zone))));
updateThing(editThing().withChannels(channels).build());
}
if (config.clockSync) {
scheduleClockSyncJob();
}
scheduleReconnectJob();
schedulePollingJob();
updateStatus(ThingStatus.UNKNOWN);
}
@Override
public void dispose() {
cancelReconnectJob();
cancelPollingJob();
cancelClockSyncJob();
closeConnection();
super.dispose();
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singletonList(NuvoThingActions.class);
}
public void handleRawCommand(@Nullable String command) {
synchronized (sequenceLock) {
try {
connector.sendCommand(command);
} catch (NuvoException e) {
logger.warn("Nuvo Command: {} failed", command);
}
}
}
/**
* Handle a command the UI
*
* @param channelUID the channel sending the command
* @param command the command received
*
*/
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
String channel = channelUID.getId();
String[] channelSplit = channel.split(CHANNEL_DELIMIT);
NuvoEnum target = NuvoEnum.valueOf(channelSplit[0].toUpperCase());
String channelType = channelSplit[1];
if (getThing().getStatus() != ThingStatus.ONLINE) {
logger.debug("Thing is not ONLINE; command {} from channel {} is ignored", command, channel);
return;
}
synchronized (sequenceLock) {
if (!connector.isConnected()) {
logger.warn("Command {} from channel {} is ignored: connection not established", command, channel);
return;
}
try {
switch (channelType) {
case CHANNEL_TYPE_POWER:
if (command instanceof OnOffType) {
connector.sendCommand(target, command == OnOffType.ON ? NuvoCommand.ON : NuvoCommand.OFF);
}
break;
case CHANNEL_TYPE_SOURCE:
if (command instanceof DecimalType) {
int value = ((DecimalType) command).intValue();
if (value >= 1 && value <= MAX_SRC) {
logger.debug("Got source command {} zone {}", value, target);
connector.sendCommand(target, NuvoCommand.SOURCE, String.valueOf(value));
}
}
break;
case CHANNEL_TYPE_VOLUME:
if (command instanceof PercentType) {
int value = (MAX_VOLUME
- (int) Math.round(
((PercentType) command).doubleValue() / 100.0 * (MAX_VOLUME - MIN_VOLUME))
+ MIN_VOLUME);
logger.debug("Got volume command {} zone {}", value, target);
connector.sendCommand(target, NuvoCommand.VOLUME, String.valueOf(value));
}
break;
case CHANNEL_TYPE_MUTE:
if (command instanceof OnOffType) {
connector.sendCommand(target,
command == OnOffType.ON ? NuvoCommand.MUTE_ON : NuvoCommand.MUTE_OFF);
}
break;
case CHANNEL_TYPE_TREBLE:
if (command instanceof DecimalType) {
int value = ((DecimalType) command).intValue();
if (value >= MIN_EQ && value <= MAX_EQ) {
// device can only accept even values
if (value % 2 == 1)
value++;
logger.debug("Got treble command {} zone {}", value, target);
connector.sendCfgCommand(target, NuvoCommand.TREBLE, String.valueOf(value));
}
}
break;
case CHANNEL_TYPE_BASS:
if (command instanceof DecimalType) {
int value = ((DecimalType) command).intValue();
if (value >= MIN_EQ && value <= MAX_EQ) {
if (value % 2 == 1)
value++;
logger.debug("Got bass command {} zone {}", value, target);
connector.sendCfgCommand(target, NuvoCommand.BASS, String.valueOf(value));
}
}
break;
case CHANNEL_TYPE_BALANCE:
if (command instanceof DecimalType) {
int value = ((DecimalType) command).intValue();
if (value >= MIN_EQ && value <= MAX_EQ) {
if (value % 2 == 1)
value++;
logger.debug("Got balance command {} zone {}", value, target);
connector.sendCfgCommand(target, NuvoCommand.BALANCE,
NuvoStatusCodes.getBalanceFromInt(value));
}
}
break;
case CHANNEL_TYPE_LOUDNESS:
if (command instanceof OnOffType) {
connector.sendCfgCommand(target, NuvoCommand.LOUDNESS,
command == OnOffType.ON ? ONE : ZERO);
}
break;
case CHANNEL_TYPE_CONTROL:
handleControlCommand(target, command);
break;
case CHANNEL_TYPE_DND:
if (command instanceof OnOffType) {
connector.sendCommand(target,
command == OnOffType.ON ? NuvoCommand.DND_ON : NuvoCommand.DND_OFF);
}
break;
case CHANNEL_TYPE_PARTY:
if (command instanceof OnOffType) {
connector.sendCommand(target,
command == OnOffType.ON ? NuvoCommand.PARTY_ON : NuvoCommand.PARTY_OFF);
}
break;
case CHANNEL_DISPLAY_LINE1:
if (command instanceof StringType) {
connector.sendCommand(target, NuvoCommand.DISPLINE1, "\"" + command + "\"");
}
break;
case CHANNEL_DISPLAY_LINE2:
if (command instanceof StringType) {
connector.sendCommand(target, NuvoCommand.DISPLINE2, "\"" + command + "\"");
}
break;
case CHANNEL_DISPLAY_LINE3:
if (command instanceof StringType) {
connector.sendCommand(target, NuvoCommand.DISPLINE3, "\"" + command + "\"");
}
break;
case CHANNEL_DISPLAY_LINE4:
if (command instanceof StringType) {
connector.sendCommand(target, NuvoCommand.DISPLINE4, "\"" + command + "\"");
}
break;
case CHANNEL_TYPE_ALLOFF:
if (command instanceof OnOffType) {
connector.sendCommand(NuvoCommand.ALLOFF);
}
break;
case CHANNEL_TYPE_ALLMUTE:
if (command instanceof OnOffType) {
connector.sendCommand(target,
command == OnOffType.ON ? NuvoCommand.ALLMUTE_ON : NuvoCommand.ALLMUTE_OFF);
}
break;
case CHANNEL_TYPE_PAGE:
if (command instanceof OnOffType) {
connector.sendCommand(target,
command == OnOffType.ON ? NuvoCommand.PAGE_ON : NuvoCommand.PAGE_OFF);
}
break;
}
} catch (NuvoException e) {
logger.warn("Command {} from channel {} failed: {}", command, channel, e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Sending command failed");
closeConnection();
scheduleReconnectJob();
}
}
}
/**
* Open the connection with the Nuvo device
*
* @return true if the connection is opened successfully or false if not
*/
private synchronized boolean openConnection() {
connector.addEventListener(this);
try {
connector.open();
} catch (NuvoException e) {
logger.debug("openConnection() failed: {}", e.getMessage());
}
logger.debug("openConnection(): {}", connector.isConnected() ? "connected" : "disconnected");
return connector.isConnected();
}
/**
* Close the connection with the Nuvo device
*/
private synchronized void closeConnection() {
if (connector.isConnected()) {
connector.close();
connector.removeEventListener(this);
logger.debug("closeConnection(): disconnected");
}
}
/**
* Handle an event received from the Nuvo device
*
* @param event the event to process
*/
@Override
public void onNewMessageEvent(NuvoMessageEvent evt) {
logger.debug("onNewMessageEvent: key {} = {}", evt.getKey(), evt.getValue());
lastEventReceived = System.currentTimeMillis();
String type = evt.getType();
String key = evt.getKey();
String updateData = evt.getValue().trim();
if (this.getThing().getStatus() == ThingStatus.OFFLINE) {
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, this.versionString);
}
switch (type) {
case TYPE_VERSION:
this.versionString = updateData;
// Determine if we are a Grand Concerto or not
if (this.versionString.contains(GC_STR)) {
this.isGConcerto = true;
connector.setEssentia(false);
}
break;
case TYPE_ALLOFF:
activeZones.forEach(zoneNum -> {
updateChannelState(NuvoEnum.valueOf(ZONE + zoneNum), CHANNEL_TYPE_POWER, OFF);
});
break;
case TYPE_ALLMUTE:
updateChannelState(NuvoEnum.SYSTEM, CHANNEL_TYPE_ALLMUTE, ONE.equals(updateData) ? ON : OFF);
activeZones.forEach(zoneNum -> {
updateChannelState(NuvoEnum.valueOf(ZONE + zoneNum), CHANNEL_TYPE_MUTE,
ONE.equals(updateData) ? ON : OFF);
});
break;
case TYPE_PAGE:
updateChannelState(NuvoEnum.SYSTEM, CHANNEL_TYPE_PAGE, ONE.equals(updateData) ? ON : OFF);
break;
case TYPE_SOURCE_UPDATE:
logger.debug("Source update: Source: {} - Value: {}", key, updateData);
NuvoEnum targetSource = NuvoEnum.valueOf(SOURCE + key);
if (updateData.contains(DISPLINE)) {
// example: DISPLINE2,"Play My Song (Featuring Dee Ajayi)"
Matcher matcher = DISP_PATTERN.matcher(updateData);
if (matcher.find()) {
updateChannelState(targetSource, CHANNEL_DISPLAY_LINE + matcher.group(1), matcher.group(2));
} else {
logger.debug("no match on message: {}", updateData);
}
} else if (updateData.contains(DISPINFO)) {
// example: DISPINFO,DUR0,POS70,STATUS2 (DUR and POS are expressed in tenths of a second)
// 6 places(tenths of a second)-> max 999,999 /10/60/60/24 = 1.15 days
Matcher matcher = DISP_INFO_PATTERN.matcher(updateData);
if (matcher.find()) {
updateChannelState(targetSource, CHANNEL_TRACK_LENGTH, matcher.group(1));
updateChannelState(targetSource, CHANNEL_TRACK_POSITION, matcher.group(2));
updateChannelState(targetSource, CHANNEL_PLAY_MODE, matcher.group(3));
} else {
logger.debug("no match on message: {}", updateData);
}
} else if (updateData.contains(NAME_QUOTE) && sourceLabels.size() <= MAX_SRC) {
// example: NAME"Ipod"
String name = updateData.split("\"")[1];
sourceLabels.add(new StateOption(key, name));
}
break;
case TYPE_ZONE_UPDATE:
logger.debug("Zone update: Zone: {} - Value: {}", key, updateData);
// example : OFF
// or: ON,SRC3,VOL63,DND0,LOCK0
// or: ON,SRC3,MUTE,DND0,LOCK0
NuvoEnum targetZone = NuvoEnum.valueOf(ZONE + key);
if (OFF.equals(updateData)) {
updateChannelState(targetZone, CHANNEL_TYPE_POWER, OFF);
updateChannelState(targetZone, CHANNEL_TYPE_SOURCE, UNDEF);
} else {
Matcher matcher = ZONE_PATTERN.matcher(updateData);
if (matcher.find()) {
updateChannelState(targetZone, CHANNEL_TYPE_POWER, ON);
updateChannelState(targetZone, CHANNEL_TYPE_SOURCE, matcher.group(1));
if (MUTE.equals(matcher.group(2))) {
updateChannelState(targetZone, CHANNEL_TYPE_MUTE, ON);
} else {
updateChannelState(targetZone, CHANNEL_TYPE_MUTE, NuvoCommand.OFF.getValue());
updateChannelState(targetZone, CHANNEL_TYPE_VOLUME, matcher.group(2).replace(VOL, BLANK));
}
updateChannelState(targetZone, CHANNEL_TYPE_DND, ONE.equals(matcher.group(3)) ? ON : OFF);
updateChannelState(targetZone, CHANNEL_TYPE_LOCK, ONE.equals(matcher.group(4)) ? ON : OFF);
} else {
logger.debug("no match on message: {}", updateData);
}
}
break;
case TYPE_ZONE_BUTTON:
logger.debug("Zone Button pressed: Source: {} - Button: {}", key, updateData);
updateChannelState(NuvoEnum.valueOf(SOURCE + key), CHANNEL_BUTTON_PRESS, updateData);
break;
case TYPE_ZONE_CONFIG:
logger.debug("Zone Configuration: Zone: {} - Value: {}", key, updateData);
// example: BASS1,TREB-2,BALR2,LOUDCMP1
Matcher matcher = ZONE_CFG_PATTERN.matcher(updateData);
if (matcher.find()) {
updateChannelState(NuvoEnum.valueOf(ZONE + key), CHANNEL_TYPE_BASS, matcher.group(1));
updateChannelState(NuvoEnum.valueOf(ZONE + key), CHANNEL_TYPE_TREBLE, matcher.group(2));
updateChannelState(NuvoEnum.valueOf(ZONE + key), CHANNEL_TYPE_BALANCE,
NuvoStatusCodes.getBalanceFromStr(matcher.group(3)));
updateChannelState(NuvoEnum.valueOf(ZONE + key), CHANNEL_TYPE_LOUDNESS,
ONE.equals(matcher.group(4)) ? ON : OFF);
} else {
logger.debug("no match on message: {}", updateData);
}
break;
default:
logger.debug("onNewMessageEvent: unhandled key {}", key);
break;
}
}
/**
* Schedule the reconnection job
*/
private void scheduleReconnectJob() {
logger.debug("Schedule reconnect job");
cancelReconnectJob();
reconnectJob = scheduler.scheduleWithFixedDelay(() -> {
if (!connector.isConnected()) {
logger.debug("Trying to reconnect...");
closeConnection();
String error = null;
if (openConnection()) {
synchronized (sequenceLock) {
try {
long prevUpdateTime = lastEventReceived;
connector.sendCommand(NuvoCommand.GET_CONTROLLER_VERSION);
NuvoEnum.VALID_SOURCES.forEach(source -> {
try {
connector.sendQuery(NuvoEnum.valueOf(source), NuvoCommand.NAME);
Thread.sleep(SLEEP_BETWEEN_CMD_MS);
connector.sendQuery(NuvoEnum.valueOf(source), NuvoCommand.DISPINFO);
Thread.sleep(SLEEP_BETWEEN_CMD_MS);
connector.sendQuery(NuvoEnum.valueOf(source), NuvoCommand.DISPLINE);
Thread.sleep(SLEEP_BETWEEN_CMD_MS);
} catch (NuvoException | InterruptedException e) {
logger.debug("Error Querying Source data: {}", e.getMessage());
}
});
// Query all active zones to get their current status and eq configuration
activeZones.forEach(zoneNum -> {
try {
connector.sendQuery(NuvoEnum.valueOf(ZONE + zoneNum), NuvoCommand.STATUS);
Thread.sleep(SLEEP_BETWEEN_CMD_MS);
connector.sendCfgCommand(NuvoEnum.valueOf(ZONE + zoneNum), NuvoCommand.EQ_QUERY,
BLANK);
Thread.sleep(SLEEP_BETWEEN_CMD_MS);
} catch (NuvoException | InterruptedException e) {
logger.debug("Error Querying Zone data: {}", e.getMessage());
}
});
// prevUpdateTime should have changed if a zone update was received
if (prevUpdateTime == lastEventReceived) {
error = "Controller not responding to status requests";
} else {
// Put the source labels on all active zones
activeZones.forEach(zoneNum -> {
stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(),
ZONE.toLowerCase() + zoneNum + CHANNEL_DELIMIT + CHANNEL_TYPE_SOURCE),
sourceLabels);
});
}
} catch (NuvoException e) {
error = "First command after connection failed";
logger.debug("{}: {}", error, e.getMessage());
}
}
} else {
error = "Reconnection failed";
}
if (error != null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error);
closeConnection();
} else {
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, this.versionString);
}
}
}, 1, RECON_POLLING_INTERVAL_SEC, TimeUnit.SECONDS);
}
/**
* Cancel the reconnection job
*/
private void cancelReconnectJob() {
ScheduledFuture<?> reconnectJob = this.reconnectJob;
if (reconnectJob != null) {
reconnectJob.cancel(true);
this.reconnectJob = null;
}
}
/**
* Schedule the polling job
*/
private void schedulePollingJob() {
logger.debug("Schedule polling job");
cancelPollingJob();
// when the Nuvo amp is off, this will keep the connection (esp Serial over IP) alive and detect if the
// connection goes down
pollingJob = scheduler.scheduleWithFixedDelay(() -> {
if (connector.isConnected()) {
logger.debug("Polling the component for updated status...");
synchronized (sequenceLock) {
try {
connector.sendCommand(NuvoCommand.GET_CONTROLLER_VERSION);
} catch (NuvoException e) {
logger.debug("Polling error: {}", e.getMessage());
}
// if the last event received was more than 1.25 intervals ago,
// the component is not responding even though the connection is still good
if ((System.currentTimeMillis() - lastEventReceived) > (POLLING_INTERVAL_SEC * 1.25 * 1000)) {
logger.debug("Component not responding to status requests");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Component not responding to status requests");
closeConnection();
scheduleReconnectJob();
}
}
}
}, INITIAL_POLLING_DELAY_SEC, POLLING_INTERVAL_SEC, TimeUnit.SECONDS);
}
/**
* Cancel the polling job
*/
private void cancelPollingJob() {
ScheduledFuture<?> pollingJob = this.pollingJob;
if (pollingJob != null) {
pollingJob.cancel(true);
this.pollingJob = null;
}
}
/**
* Schedule the clock sync job
*/
private void scheduleClockSyncJob() {
logger.debug("Schedule clock sync job");
cancelClockSyncJob();
clockSyncJob = scheduler.scheduleWithFixedDelay(() -> {
if (this.isGConcerto) {
try {
connector.sendCommand(NuvoCommand.CFGTIME.getValue() + DATE_FORMAT.format(new Date()));
} catch (NuvoException e) {
logger.debug("Error syncing clock: {}", e.getMessage());
}
} else {
this.cancelClockSyncJob();
}
}, INITIAL_CLOCK_SYNC_DELAY_SEC, CLOCK_SYNC_INTERVAL_SEC, TimeUnit.SECONDS);
}
/**
* Cancel the clock sync job
*/
private void cancelClockSyncJob() {
ScheduledFuture<?> clockSyncJob = this.clockSyncJob;
if (clockSyncJob != null) {
clockSyncJob.cancel(true);
this.clockSyncJob = null;
}
}
/**
* Update the state of a channel
*
* @param target the channel group
* @param channelType the channel group item
* @param value the value to be updated
*/
private void updateChannelState(NuvoEnum target, String channelType, String value) {
String channel = target.name().toLowerCase() + CHANNEL_DELIMIT + channelType;
if (!isLinked(channel)) {
return;
}
State state = UnDefType.UNDEF;
if (UNDEF.equals(value)) {
updateState(channel, state);
return;
}
switch (channelType) {
case CHANNEL_TYPE_POWER:
case CHANNEL_TYPE_MUTE:
case CHANNEL_TYPE_DND:
case CHANNEL_TYPE_PARTY:
case CHANNEL_TYPE_ALLMUTE:
case CHANNEL_TYPE_PAGE:
case CHANNEL_TYPE_LOUDNESS:
state = ON.equals(value) ? OnOffType.ON : OnOffType.OFF;
break;
case CHANNEL_TYPE_LOCK:
state = ON.equals(value) ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
break;
case CHANNEL_TYPE_SOURCE:
case CHANNEL_TYPE_TREBLE:
case CHANNEL_TYPE_BASS:
case CHANNEL_TYPE_BALANCE:
state = new DecimalType(value);
break;
case CHANNEL_TYPE_VOLUME:
int volume = Integer.parseInt(value);
long volumePct = Math
.round((double) (MAX_VOLUME - volume) / (double) (MAX_VOLUME - MIN_VOLUME) * 100.0);
state = new PercentType(BigDecimal.valueOf(volumePct));
break;
case CHANNEL_DISPLAY_LINE1:
case CHANNEL_DISPLAY_LINE2:
case CHANNEL_DISPLAY_LINE3:
case CHANNEL_DISPLAY_LINE4:
case CHANNEL_BUTTON_PRESS:
state = new StringType(value);
break;
case CHANNEL_PLAY_MODE:
state = new StringType(NuvoStatusCodes.PLAY_MODE.get(value));
break;
case CHANNEL_TRACK_LENGTH:
case CHANNEL_TRACK_POSITION:
state = new QuantityType<Time>(Integer.parseInt(value) / 10, NuvoHandler.API_SECOND_UNIT);
break;
default:
break;
}
updateState(channel, state);
}
/**
* Handle a button press from a UI Player item
*
* @param target the nuvo zone to receive the command
* @param command the button press command to send to the zone
*/
private void handleControlCommand(NuvoEnum target, Command command) throws NuvoException {
if (command instanceof PlayPauseType) {
connector.sendCommand(target, NuvoCommand.PLAYPAUSE);
} else if (command instanceof NextPreviousType) {
if (command == NextPreviousType.NEXT) {
connector.sendCommand(target, NuvoCommand.NEXT);
} else if (command == NextPreviousType.PREVIOUS) {
connector.sendCommand(target, NuvoCommand.PREV);
}
} else {
logger.warn("Unknown control command: {}", command);
}
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="nuvo" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Nuvo Whole House Audio Binding</name>
<description>Controls the Nuvo Grand Concerto or Essentia G Whole House Amplifier.</description>
<author>Michael Lobstein</author>
</binding:binding>

View File

@@ -0,0 +1,328 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="nuvo"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- Nuvo Whole House Amplifier Thing -->
<thing-type id="amplifier">
<label>Whole House Amplifier</label>
<description>
A Multi-zone Whole House Amplifier System
</description>
<channel-groups>
<channel-group id="system" typeId="system">
<label>System</label>
<description>System Level Commands</description>
</channel-group>
<channel-group id="zone1" typeId="zone">
<label>Zone 1</label>
<description>The Controls for Zone 1</description>
</channel-group>
<channel-group id="zone2" typeId="zone">
<label>Zone 2</label>
<description>The Controls for Zone 2</description>
</channel-group>
<channel-group id="zone3" typeId="zone">
<label>Zone 3</label>
<description>The Controls for Zone 3</description>
</channel-group>
<channel-group id="zone4" typeId="zone">
<label>Zone 4</label>
<description>The Controls for Zone 4</description>
</channel-group>
<channel-group id="zone5" typeId="zone">
<label>Zone 5</label>
<description>The Controls for Zone 5</description>
</channel-group>
<channel-group id="zone6" typeId="zone">
<label>Zone 6</label>
<description>The Controls for Zone 6</description>
</channel-group>
<channel-group id="zone7" typeId="zone">
<label>Zone 7</label>
<description>The Controls for Zone 7</description>
</channel-group>
<channel-group id="zone8" typeId="zone">
<label>Zone 8</label>
<description>The Controls for Zone 8</description>
</channel-group>
<channel-group id="zone9" typeId="zone">
<label>Zone 9</label>
<description>The Controls for Zone 9</description>
</channel-group>
<channel-group id="zone10" typeId="zone">
<label>Zone 10</label>
<description>The Controls for Zone 10</description>
</channel-group>
<channel-group id="zone11" typeId="zone">
<label>Zone 11</label>
<description>The Controls for Zone 11</description>
</channel-group>
<channel-group id="zone12" typeId="zone">
<label>Zone 12</label>
<description>The Controls for Zone 12</description>
</channel-group>
<channel-group id="zone13" typeId="zone">
<label>Zone 13</label>
<description>The Controls for Zone 13</description>
</channel-group>
<channel-group id="zone14" typeId="zone">
<label>Zone 14</label>
<description>The Controls for Zone 14</description>
</channel-group>
<channel-group id="zone15" typeId="zone">
<label>Zone 15</label>
<description>The Controls for Zone 15</description>
</channel-group>
<channel-group id="zone16" typeId="zone">
<label>Zone 16</label>
<description>The Controls for Zone 16</description>
</channel-group>
<channel-group id="zone17" typeId="zone">
<label>Zone 17</label>
<description>The Controls for Zone 17</description>
</channel-group>
<channel-group id="zone18" typeId="zone">
<label>Zone 18</label>
<description>The Controls for Zone 18</description>
</channel-group>
<channel-group id="zone19" typeId="zone">
<label>Zone 19</label>
<description>The Controls for Zone 19</description>
</channel-group>
<channel-group id="zone20" typeId="zone">
<label>Zone 20</label>
<description>The Controls for Zone 20</description>
</channel-group>
<channel-group id="source1" typeId="source">
<label>Source 1</label>
<description>The Display Information for Source 1</description>
</channel-group>
<channel-group id="source2" typeId="source">
<label>Source 2</label>
<description>The Display Information for Source 2</description>
</channel-group>
<channel-group id="source3" typeId="source">
<label>Source 3</label>
<description>The Display Information for Source 3</description>
</channel-group>
<channel-group id="source4" typeId="source">
<label>Source 4</label>
<description>The Display Information for Source 4</description>
</channel-group>
<channel-group id="source5" typeId="source">
<label>Source 5</label>
<description>The Display Information for Source 5</description>
</channel-group>
<channel-group id="source6" typeId="source">
<label>Source 6</label>
<description>The Display Information for Source 6</description>
</channel-group>
</channel-groups>
<config-description>
<parameter name="serialPort" type="text" required="false">
<context>serial-port</context>
<label>Serial Port</label>
<description>Serial Port to use for connecting to the Nuvo amplifier</description>
</parameter>
<parameter name="host" type="text" required="false">
<context>network-address</context>
<label>Address</label>
<description>Host Name or IP Address of the machine connected to the Nuvo amplifier (Serial over IP)</description>
</parameter>
<parameter name="port" type="integer" min="1" max="65535" required="false">
<label>Port</label>
<description>Communication Port (serial over IP). For IP connection to the Nuvo amplifier</description>
<default>4444</default>
</parameter>
<parameter name="numZones" type="integer" min="1" max="20" required="true">
<label>Number of Zones</label>
<description>Number of Zones on the amplifier to utilize in the binding (Up to 20 zones when using expansion module)</description>
<default>6</default>
</parameter>
<parameter name="clockSync" type="boolean" required="false">
<label>Sync Clock On GConcerto</label>
<description>If set to true, the binding will sync the internal clock on the Grand Concerto to match the openHAB
host's system clock. The sync job runs at binding startup and once an hour thereafter. The Essentia G has no RTC,
so this setting has no effect on that component.</description>
<default>false</default>
</parameter>
</config-description>
</thing-type>
<channel-group-type id="system">
<label>System</label>
<description>System Level Commands</description>
<channels>
<channel id="alloff" typeId="alloff"/>
<channel id="allmute" typeId="system.mute"/>
<channel id="page" typeId="page"/>
</channels>
</channel-group-type>
<channel-group-type id="zone">
<label>Zone Controls</label>
<description>The Controls for the Zone</description>
<channels>
<channel id="power" typeId="system.power"/>
<channel id="source" typeId="source"/>
<channel id="volume" typeId="system.volume"/>
<channel id="mute" typeId="system.mute"/>
<channel id="control" typeId="control"/>
<channel id="treble" typeId="treble"/>
<channel id="bass" typeId="bass"/>
<channel id="balance" typeId="balance"/>
<channel id="loudness" typeId="loudness"/>
<channel id="dnd" typeId="dnd"/>
<channel id="lock" typeId="lock"/>
<channel id="party" typeId="party"/>
</channels>
</channel-group-type>
<channel-group-type id="source">
<label>Source Info</label>
<description>The Display Information for the Source</description>
<channels>
<channel id="display_line1" typeId="display_line1"/>
<channel id="display_line2" typeId="display_line2"/>
<channel id="display_line3" typeId="display_line3"/>
<channel id="display_line4" typeId="display_line4"/>
<channel id="play_mode" typeId="play_mode"/>
<channel id="track_length" typeId="track_length"/>
<channel id="track_position" typeId="track_position"/>
<channel id="button_press" typeId="button_press"/>
</channels>
</channel-group-type>
<channel-type id="alloff">
<item-type>Switch</item-type>
<label>All Off</label>
<description>Turn All Zones Off</description>
</channel-type>
<channel-type id="page">
<item-type>Switch</item-type>
<label>Page</label>
<description>Activates the Page Mode for All Zones</description>
</channel-type>
<channel-type id="source">
<item-type>Number</item-type>
<label>Source Input</label>
<description>Select the Source Input for the Zone</description>
</channel-type>
<channel-type id="control">
<item-type>Player</item-type>
<label>Control</label>
<description>Transport Controls e.g. Play/Pause/Next/Previous for the Current Source</description>
<category>Player</category>
</channel-type>
<channel-type id="treble">
<item-type>Number</item-type>
<label>Treble Adjustment</label>
<description>Adjust the Treble Setting for the Zone</description>
<state min="-18" max="18" step="2" pattern="%d"/>
</channel-type>
<channel-type id="bass">
<item-type>Number</item-type>
<label>Bass Adjustment</label>
<description>Adjust the Bass Setting for the Zone</description>
<state min="-18" max="18" step="2" pattern="%d"/>
</channel-type>
<channel-type id="balance">
<item-type>Number</item-type>
<label>Balance Adjustment</label>
<description>Adjust the Balance Setting for the Zone</description>
<state min="-18" max="18" step="2" pattern="%d"/>
</channel-type>
<channel-type id="loudness">
<item-type>Switch</item-type>
<label>Loudness Compensation</label>
<description>A Switch That Controls the Loudness Compensation Setting for the Zone</description>
</channel-type>
<channel-type id="dnd">
<item-type>Switch</item-type>
<label>Do Not Disturb</label>
<description>A Switch That Controls If the Zone Should Ignore an Incoming Audio Page</description>
</channel-type>
<channel-type id="lock">
<item-type>Contact</item-type>
<label>Locked</label>
<description>Indicates If This Zone Is Locked</description>
<state readOnly="true">
<options>
<option value="CLOSED">Unlocked</option>
<option value="OPEN">Locked</option>
</options>
</state>
</channel-type>
<channel-type id="party">
<item-type>Switch</item-type>
<label>Party Mode</label>
<description>Activate Party Mode With This Zone as the Host</description>
</channel-type>
<channel-type id="display_line1">
<item-type>String</item-type>
<label>Display Line 1</label>
<description>1st Line of Text Being Displayed on the Keypad</description>
</channel-type>
<channel-type id="display_line2">
<item-type>String</item-type>
<label>Display Line 2</label>
<description>2nd Line of Text Being Displayed on the Keypad</description>
</channel-type>
<channel-type id="display_line3">
<item-type>String</item-type>
<label>Display Line 3</label>
<description>3rd Line of Text Being Displayed on the Keypad</description>
</channel-type>
<channel-type id="display_line4">
<item-type>String</item-type>
<label>Display Line 4</label>
<description>4th Line of Text Being Displayed on the Keypad</description>
</channel-type>
<channel-type id="play_mode">
<item-type>String</item-type>
<label>Play Mode</label>
<description>The Current Playback Mode of the Source</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="track_length">
<item-type>Number:Time</item-type>
<label>Track Length</label>
<description>The Total Running Time of the Current Playing Track</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="track_position">
<item-type>Number:Time</item-type>
<label>Track Position</label>
<description>The Running Time Elapsed of the Current Playing Track</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="button_press">
<item-type>String</item-type>
<label>Button Pressed</label>
<description>Indicates the Last Button Pressed On the Keypad for a Non NuvoNet Source</description>
<state readOnly="true"/>
</channel-type>
</thing:thing-descriptions>