diff --git a/CODEOWNERS b/CODEOWNERS index 74655f003f83c..b57cd877da767 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -150,6 +150,7 @@ /bundles/org.openhab.binding.novafinedust/ @t2000 /bundles/org.openhab.binding.ntp/ @marcelrv /bundles/org.openhab.binding.nuki/ @mkatter +/bundles/org.openhab.binding.nuvo/ @mlobstein /bundles/org.openhab.binding.oceanic/ @kgoderis /bundles/org.openhab.binding.ojelectronics/ @EvilPingu /bundles/org.openhab.binding.omnikinverter/ @hansbogert diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 8071dab4bbb5e..9bbe6a0793374 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -734,6 +734,11 @@ org.openhab.binding.nuki ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.nuvo + ${project.version} + org.openhab.addons.bundles org.openhab.binding.oceanic diff --git a/bundles/org.openhab.binding.nuvo/.classpath b/bundles/org.openhab.binding.nuvo/.classpath new file mode 100644 index 0000000000000..d223a57c7967a --- /dev/null +++ b/bundles/org.openhab.binding.nuvo/.classpath @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.nuvo/.project b/bundles/org.openhab.binding.nuvo/.project new file mode 100644 index 0000000000000..310fe01fb4c3b --- /dev/null +++ b/bundles/org.openhab.binding.nuvo/.project @@ -0,0 +1,23 @@ + + + org.openhab.binding.nuvo + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/bundles/org.openhab.binding.nuvo/NOTICE b/bundles/org.openhab.binding.nuvo/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.nuvo/NOTICE @@ -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 diff --git a/bundles/org.openhab.binding.nuvo/README.md b/bundles/org.openhab.binding.nuvo/README.md new file mode 100644 index 0000000000000..09a785a500cf8 --- /dev/null +++ b/bundles/org.openhab.binding.nuvo/README.md @@ -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 + +``` diff --git a/bundles/org.openhab.binding.nuvo/doc/nuvo_logo.png b/bundles/org.openhab.binding.nuvo/doc/nuvo_logo.png new file mode 100644 index 0000000000000..88264aa680a54 Binary files /dev/null and b/bundles/org.openhab.binding.nuvo/doc/nuvo_logo.png differ diff --git a/bundles/org.openhab.binding.nuvo/pom.xml b/bundles/org.openhab.binding.nuvo/pom.xml new file mode 100644 index 0000000000000..fd4e0cec8442e --- /dev/null +++ b/bundles/org.openhab.binding.nuvo/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 2.5.9-SNAPSHOT + + + org.openhab.binding.nuvo + + openHAB Add-ons :: Bundles :: Nuvo Binding + + diff --git a/bundles/org.openhab.binding.nuvo/src/main/feature/feature.xml b/bundles/org.openhab.binding.nuvo/src/main/feature/feature.xml new file mode 100644 index 0000000000000..41cc3dcb76511 --- /dev/null +++ b/bundles/org.openhab.binding.nuvo/src/main/feature/feature.xml @@ -0,0 +1,10 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + openhab-transport-serial + mvn:org.openhab.addons.bundles/org.openhab.binding.nuvo/${project.version} + + diff --git a/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/INuvoThingActions.java b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/INuvoThingActions.java new file mode 100644 index 0000000000000..8bec3d20d3a05 --- /dev/null +++ b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/INuvoThingActions.java @@ -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); +} diff --git a/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/NuvoBindingConstants.java b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/NuvoBindingConstants.java new file mode 100644 index 0000000000000..e5648e3d25b10 --- /dev/null +++ b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/NuvoBindingConstants.java @@ -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.eclipse.smarthome.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"; +} diff --git a/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/NuvoException.java b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/NuvoException.java new file mode 100644 index 0000000000000..26b18f7620c81 --- /dev/null +++ b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/NuvoException.java @@ -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); + } +} diff --git a/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/NuvoHandlerFactory.java b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/NuvoHandlerFactory.java new file mode 100644 index 0000000000000..d7913c4b34483 --- /dev/null +++ b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/NuvoHandlerFactory.java @@ -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.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory; +import org.eclipse.smarthome.io.transport.serial.SerialPortManager; +import org.openhab.binding.nuvo.internal.handler.NuvoHandler; +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 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; + } +} diff --git a/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/NuvoStateDescriptionOptionProvider.java b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/NuvoStateDescriptionOptionProvider.java new file mode 100644 index 0000000000000..f231af318223e --- /dev/null +++ b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/NuvoStateDescriptionOptionProvider.java @@ -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.eclipse.smarthome.core.thing.binding.BaseDynamicStateDescriptionProvider; +import org.eclipse.smarthome.core.thing.i18n.ChannelTypeI18nLocalizationService; +import org.eclipse.smarthome.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; + } +} diff --git a/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/NuvoThingActions.java b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/NuvoThingActions.java new file mode 100644 index 0000000000000..4dda8cd1aaf3a --- /dev/null +++ b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/NuvoThingActions.java @@ -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.eclipse.smarthome.core.thing.binding.ThingActions; +import org.eclipse.smarthome.core.thing.binding.ThingActionsScope; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.openhab.binding.nuvo.internal.handler.NuvoHandler; +import org.openhab.core.automation.annotation.ActionInput; +import org.openhab.core.automation.annotation.RuleAction; +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"); + } +} diff --git a/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoCommand.java b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoCommand.java new file mode 100644 index 0000000000000..ac43e707eeeba --- /dev/null +++ b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoCommand.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoConnector.java b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoConnector.java new file mode 100644 index 0000000000000..e47db3229f34c --- /dev/null +++ b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoConnector.java @@ -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 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)); + } +} diff --git a/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoDefaultConnector.java b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoDefaultConnector.java new file mode 100644 index 0000000000000..f9fc45dbf0ac5 --- /dev/null +++ b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoDefaultConnector.java @@ -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); + } +} diff --git a/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoEnum.java b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoEnum.java new file mode 100644 index 0000000000000..a52bfa3f1f73a --- /dev/null +++ b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoEnum.java @@ -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 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; + } +} diff --git a/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoIpConnector.java b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoIpConnector.java new file mode 100644 index 0000000000000..d4a51fc7264a2 --- /dev/null +++ b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoIpConnector.java @@ -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); + } + } +} diff --git a/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoMessageEvent.java b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoMessageEvent.java new file mode 100644 index 0000000000000..64b334c8283b0 --- /dev/null +++ b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoMessageEvent.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoMessageEventListener.java b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoMessageEventListener.java new file mode 100644 index 0000000000000..481166666bccd --- /dev/null +++ b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoMessageEventListener.java @@ -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); +} diff --git a/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoReaderThread.java b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoReaderThread.java new file mode 100644 index 0000000000000..3e6b8f39cd2a9 --- /dev/null +++ b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoReaderThread.java @@ -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"); + } +} diff --git a/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoSerialConnector.java b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoSerialConnector.java new file mode 100644 index 0000000000000..51dd3214e749c --- /dev/null +++ b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoSerialConnector.java @@ -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.eclipse.smarthome.io.transport.serial.PortInUseException; +import org.eclipse.smarthome.io.transport.serial.SerialPort; +import org.eclipse.smarthome.io.transport.serial.SerialPortIdentifier; +import org.eclipse.smarthome.io.transport.serial.SerialPortManager; +import org.eclipse.smarthome.io.transport.serial.UnsupportedCommOperationException; +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 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"); + } +} diff --git a/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoStatusCodes.java b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoStatusCodes.java new file mode 100644 index 0000000000000..2cbd33c7ed729 --- /dev/null +++ b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoStatusCodes.java @@ -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 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; + } +} diff --git a/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/configuration/NuvoThingConfiguration.java b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/configuration/NuvoThingConfiguration.java new file mode 100644 index 0000000000000..75367dc79f488 --- /dev/null +++ b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/configuration/NuvoThingConfiguration.java @@ -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; +} diff --git a/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/handler/NuvoHandler.java b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/handler/NuvoHandler.java new file mode 100644 index 0000000000000..97f653407814b --- /dev/null +++ b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/handler/NuvoHandler.java @@ -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.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.NextPreviousType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.OpenClosedType; +import org.eclipse.smarthome.core.library.types.PercentType; +import org.eclipse.smarthome.core.library.types.PlayPauseType; +import org.eclipse.smarthome.core.library.types.QuantityType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.library.unit.SmartHomeUnits; +import org.eclipse.smarthome.core.thing.Channel; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerService; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.State; +import org.eclipse.smarthome.core.types.StateOption; +import org.eclipse.smarthome.core.types.UnDefType; +import org.eclipse.smarthome.io.transport.serial.SerialPortManager; +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.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