From bc9cf8e07ad235c1827e15a9c2874120463d4126 Mon Sep 17 00:00:00 2001 From: Espen Fossen Date: Mon, 22 Aug 2022 23:27:24 +0200 Subject: [PATCH] [nobohub] Initial contribution (#12937) * Added NoboHub binding. Signed-off-by: Espen Fossen --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + bundles/org.openhab.binding.nobohub/NOTICE | 13 + bundles/org.openhab.binding.nobohub/README.md | 167 +++++++ .../doc/nobohub.jpg | Bin 0 -> 4515 bytes bundles/org.openhab.binding.nobohub/pom.xml | 17 + .../src/main/feature/feature.xml | 9 + .../internal/ComponentConfiguration.java | 31 ++ .../nobohub/internal/ComponentHandler.java | 159 +++++++ .../binding/nobohub/internal/Helpers.java | 34 ++ .../internal/NoboHubBindingConstants.java | 116 +++++ .../internal/NoboHubBridgeConfiguration.java | 42 ++ .../internal/NoboHubBridgeHandler.java | 418 ++++++++++++++++++ .../internal/NoboHubConfiguration.java | 43 ++ .../internal/NoboHubHandlerFactory.java | 132 ++++++ .../internal/NoboHubTranslationProvider.java | 66 +++ ...rofileStateDescriptionOptionsProvider.java | 42 ++ .../nobohub/internal/ZoneConfiguration.java | 29 ++ .../binding/nobohub/internal/ZoneHandler.java | 235 ++++++++++ .../connection/HubCommunicationThread.java | 167 +++++++ .../internal/connection/HubConnection.java | 270 +++++++++++ .../discovery/NoboHubDiscoveryService.java | 163 +++++++ .../discovery/NoboThingDiscoveryService.java | 161 +++++++ .../nobohub/internal/model/Component.java | 102 +++++ .../internal/model/ComponentRegister.java | 67 +++ .../binding/nobohub/internal/model/Hub.java | 104 +++++ .../nobohub/internal/model/ModelHelper.java | 82 ++++ .../model/NoboCommunicationException.java | 34 ++ .../internal/model/NoboDataException.java | 34 ++ .../nobohub/internal/model/OverrideMode.java | 73 +++ .../nobohub/internal/model/OverridePlan.java | 100 +++++ .../internal/model/OverrideRegister.java | 62 +++ .../internal/model/OverrideTarget.java | 52 +++ .../nobohub/internal/model/OverrideType.java | 55 +++ .../nobohub/internal/model/SerialNumber.java | 116 +++++ .../nobohub/internal/model/Temperature.java | 66 +++ .../nobohub/internal/model/WeekProfile.java | 117 +++++ .../internal/model/WeekProfileRegister.java | 75 ++++ .../internal/model/WeekProfileStatus.java | 59 +++ .../binding/nobohub/internal/model/Zone.java | 107 +++++ .../nobohub/internal/model/ZoneRegister.java | 67 +++ .../main/resources/OH-INF/binding/binding.xml | 9 + .../resources/OH-INF/i18n/nobohub.properties | 57 +++ .../OH-INF/i18n/nobohub_no.properties | 57 +++ .../main/resources/OH-INF/thing/bridge.xml | 38 ++ .../resources/OH-INF/thing/thing-types.xml | 137 ++++++ .../internal/model/ComponentRegisterTest.java | 79 ++++ .../nobohub/internal/model/ComponentTest.java | 46 ++ .../nobohub/internal/model/HubTest.java | 69 +++ .../internal/model/ModelHelperTest.java | 85 ++++ .../model/OverridePlanRegisterTest.java | 69 +++ .../internal/model/OverridePlanTest.java | 90 ++++ .../internal/model/SerialNumberTest.java | 55 +++ .../internal/model/TemperatureTest.java | 41 ++ .../model/WeekProfileRegisterTest.java | 82 ++++ .../internal/model/WeekProfileTest.java | 95 ++++ .../internal/model/ZoneRegisterTest.java | 80 ++++ .../nobohub/internal/model/ZoneTest.java | 46 ++ bundles/pom.xml | 1 + 59 files changed, 4828 insertions(+) create mode 100644 bundles/org.openhab.binding.nobohub/NOTICE create mode 100644 bundles/org.openhab.binding.nobohub/README.md create mode 100644 bundles/org.openhab.binding.nobohub/doc/nobohub.jpg create mode 100644 bundles/org.openhab.binding.nobohub/pom.xml create mode 100644 bundles/org.openhab.binding.nobohub/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/ComponentConfiguration.java create mode 100644 bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/ComponentHandler.java create mode 100644 bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/Helpers.java create mode 100644 bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/NoboHubBindingConstants.java create mode 100644 bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/NoboHubBridgeConfiguration.java create mode 100644 bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/NoboHubBridgeHandler.java create mode 100644 bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/NoboHubConfiguration.java create mode 100644 bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/NoboHubHandlerFactory.java create mode 100644 bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/NoboHubTranslationProvider.java create mode 100644 bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/WeekProfileStateDescriptionOptionsProvider.java create mode 100644 bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/ZoneConfiguration.java create mode 100644 bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/ZoneHandler.java create mode 100644 bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/connection/HubCommunicationThread.java create mode 100644 bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/connection/HubConnection.java create mode 100644 bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/discovery/NoboHubDiscoveryService.java create mode 100644 bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/discovery/NoboThingDiscoveryService.java create mode 100644 bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/Component.java create mode 100644 bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/ComponentRegister.java create mode 100644 bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/Hub.java create mode 100644 bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/ModelHelper.java create mode 100644 bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/NoboCommunicationException.java create mode 100644 bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/NoboDataException.java create mode 100644 bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/OverrideMode.java create mode 100644 bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/OverridePlan.java create mode 100644 bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/OverrideRegister.java create mode 100644 bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/OverrideTarget.java create mode 100644 bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/OverrideType.java create mode 100644 bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/SerialNumber.java create mode 100644 bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/Temperature.java create mode 100644 bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/WeekProfile.java create mode 100644 bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/WeekProfileRegister.java create mode 100644 bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/WeekProfileStatus.java create mode 100644 bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/Zone.java create mode 100644 bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/ZoneRegister.java create mode 100644 bundles/org.openhab.binding.nobohub/src/main/resources/OH-INF/binding/binding.xml create mode 100644 bundles/org.openhab.binding.nobohub/src/main/resources/OH-INF/i18n/nobohub.properties create mode 100644 bundles/org.openhab.binding.nobohub/src/main/resources/OH-INF/i18n/nobohub_no.properties create mode 100644 bundles/org.openhab.binding.nobohub/src/main/resources/OH-INF/thing/bridge.xml create mode 100644 bundles/org.openhab.binding.nobohub/src/main/resources/OH-INF/thing/thing-types.xml create mode 100644 bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/ComponentRegisterTest.java create mode 100644 bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/ComponentTest.java create mode 100644 bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/HubTest.java create mode 100644 bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/ModelHelperTest.java create mode 100644 bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/OverridePlanRegisterTest.java create mode 100644 bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/OverridePlanTest.java create mode 100644 bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/SerialNumberTest.java create mode 100644 bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/TemperatureTest.java create mode 100644 bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/WeekProfileRegisterTest.java create mode 100644 bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/WeekProfileTest.java create mode 100644 bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/ZoneRegisterTest.java create mode 100644 bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/ZoneTest.java diff --git a/CODEOWNERS b/CODEOWNERS index 7d4e10888c3..316b6341670 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -223,6 +223,7 @@ /bundles/org.openhab.binding.nibeuplink/ @alexf2015 /bundles/org.openhab.binding.nikobus/ @crnjan /bundles/org.openhab.binding.nikohomecontrol/ @mherwege +/bundles/org.openhab.binding.nobohub/ @espenaf /bundles/org.openhab.binding.novafinedust/ @t2000 /bundles/org.openhab.binding.ntp/ @marcelrv /bundles/org.openhab.binding.nuki/ @janvyb diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 688265456c2..f7cb83f0ef5 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -1111,6 +1111,11 @@ org.openhab.binding.nikohomecontrol ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.nobohub + ${project.version} + org.openhab.addons.bundles org.openhab.binding.novafinedust diff --git a/bundles/org.openhab.binding.nobohub/NOTICE b/bundles/org.openhab.binding.nobohub/NOTICE new file mode 100644 index 00000000000..38d625e3492 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/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.nobohub/README.md b/bundles/org.openhab.binding.nobohub/README.md new file mode 100644 index 00000000000..01b671d2e35 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/README.md @@ -0,0 +1,167 @@ +# NoboHub Binding + +This binding controls the Glen Dimplex Nobø Hub using the Nobø Hub API v1.1. + +![Nobo Hub](doc/nobohub.jpg) + +It lets you read and change temperature and profile settings for zones, and read and set active overrides to change the global mode of the hub. + +This binding is tested with the following devices: + +* Thermostats for different electrical panel heaters +* Thermostats for heating in floors +* Nobø Switch SW 4 + +## Thermostats + +Not all thermostats are made equal. + +* NCU-1R: Comfort temperature setting on the device overrides values from the Hub, making the setting in the Hub useless. +* NCU-2R: Synchronizes temperature settings to and from the Hub. + +## Supported Things + +| Thing | Thing Type | Description | +|-----------|------------|-------------------------------------------------------------------------------------------------| +| hub | Bridge | The Nobø Hub provides a gateway between your components, with the ability to organise in zones. | +| component | Thing | A component is a device, i.e. panel heater or switch. | +| zone | Thing | A zone can hold one or more components. | + + +## Discovery + +The hub will be automatically discovered. +Before it can be used, you will have to update the configuration with the last three digits of its serial number. + +When the hub is configured with the correct serial number, it will autodetect zones and components (thermostats and switches). + +## Thing Configuration + +``` +# Configuration for Nobø Hub +# +# Serial number of the Nobø hub to communicate with, 12 digits. +serialNumber=103000xxxxxx + +# Host name or IP address of the Nobø hub +hostName=10.0.0.10 +``` + +## Channels + +### Hub + +| channel | type | description | +|---------------------|--------|-----------------------------------------------------| +| activeOverrideName | String | The name of the active override | + +### Zone + +| channel | type | description | +|------------------------------|--------------------|--------------------------------------------| +| activeWeekProfileName | String | The name of the active week profile | +| activeWeekProfile | Number | The active week profile id | +| comfortTemperature | Number:Temperature | The configured comfort temperature | +| ecoTemperature | Number:Temperature | The configured eco temparature | +| currentTemperature | Number:Temperature | The current temperature in the zone | +| calculatedWeekProfileStatus | String | The current override based on week profile | + +CurrentTemperature only works if the zone has a device that reports it (e.g. a switch). + +### Component + +| channel | type | description | +|---------------------|--------------------|------------------------------------------| +| currentTemperature | Number:Temperature | The current temperature of the component | + +Not all devices report this. + +## Full Example + +### nobo.things + +``` +Bridge nobohub:nobohub:controller "Nobø Hub" [ hostName="192.168.1.10", serialNumber="103000000000" ] { + Thing zone 1 "Zone - Kitchen" [ id=1 ] + Thing component 184000000000 "Heater - Kitchen" [ serialNumber="184000000000" ] +} +``` + +### nobo.items + +``` +// Hub +String Nobo_Hub_GlobalOverride "Global Override %s" {channel="nobohub:nobohub:controller:activeOverrideName"} + +// Panel Heater +Number:Temperature PanelHeater_CurrentTemperature "Setpoint [%.1f °C]" {channel="nobohub:component:controller:184000000000:currentTemperature"} + +// Zone +String Zone_ActiveWeekProfileName "Active week profile name [%s]" {channel="nobohub:zone:controller:1:activeWeekProfileName"} +Number Zone_ActiveWeekProfile "Active week profile [%d]" {channel="nobohub:zone:controller:1:activeWeekProfile"} +String Zone_ActiveStatus "Active status %s]" {channel="nobohub:zone:controller:1:calculatedWeekProfileStatus"} +Number:Temperature Zone_ComfortTemperature "Comfort temperature [%.1f °C]" {channel="nobohub:zone:controller:1:comfortTemperature"} +Number:Temperature Zone_EcoTemperatur "Eco temperature [%.1f °C]" {channel="nobohub:zone:controller:1:ecoTemperature"} +Number:Temperature Zone_CurrentTemperature "Current temperature [%.1f °C]" {channel="nobohub:zone:controller:1:currentTemperature"} +``` + +### nobo.sitemap + +``` +sitemap nobo label="Nobø " { + + Frame label="Hub"{ + Switch item=Nobo_Hub_GlobalOverride + } + + Frame label="Main Bedroom"{ + Switch item=Zone_ActiveStatus + Text item=Zone_ActiveWeekProfileName + Text item=Zone_ActiveWeekProfile + Selection item=Zone_ActiveWeekProfile + Setpoint item=Zone_ComfortTemperatur minValue=7 maxValue=30 step=1 icon="temperature" + Setpoint item=Zone_EcoTemperatur minValue=7 maxValue=30 step=1 icon="temperature" + Text item=Zone_CurrentTemperatur + Text item=PanelHeater_CurrentTemperatur + } +} +``` + +## Organize your setup + +Nobø Hub uses a combination of status types (Normal, Comfort, Eco, Away), profiles types (Comfort, Eco, Away, Off), predefined temperature types (Comfort, Eco, Away), zones and override settings to organize and enable different features. +This makes it possible to control the heaters in many different scenarios and combinations. +The following is a suggested way of organizing the binding with the Hub for a good level of control and flexibility. + +If you own panels with a physical Comfort temperature override, you need to use the Eco temperature type for setting level used by the day based profiles. +If not, you can use either Comfort or Eco to set wanted level. + +Start by creating the following profiles in the Nobø Hub App: + + OFF Set to status off all day, every day. + ON Set to status [Comfort|Eco] all day, every day + Eco Set to status Eco all day, every day + Away Set to status Away all way, every day + Weekday 06->16 Set to status [Comfort|Eco] between 06->16 every weekday, otherwise set to [Away|Off] + Weekday 06->23 Set to status [Comfort|Eco] between 06->23 every weekday, otherwise set to [Away|Off] + Weekend 06->16 Set to status [Comfort|Eco] between 06->16 in the weekend, otherwise set to [Away|Off] + Weekend 06->23 Set to status [Comfort|Eco] between 06->23 in the weekend, otherwise set to [Away|Off] + Every day 06->16 Set to status [Comfort|Eco] between 06->16 every day, otherwise set to [Away|Off] + Every day 06->23 Set to status [Comfort|Eco] between 06->23 every day, otherwise set to [Away|Off] + +Next set [Comfort|Eco] level for each zone to your requirements. +For a more advanced setup, you can create a rule which both sets temperature level and profile. + +Then create a sitemap with a Selection pointing to the Week Profile item. +The binding will now automatically update all available week profile options in the selection button: + +### nobo.sitemap + +``` +sitemap nobo label="Nobø " { + + Frame label="Main Bedroom"{ + Selection item=MainBedroom_Zone_WeekProfile + } +} +``` diff --git a/bundles/org.openhab.binding.nobohub/doc/nobohub.jpg b/bundles/org.openhab.binding.nobohub/doc/nobohub.jpg new file mode 100644 index 0000000000000000000000000000000000000000..18915f28bca19af0f57cc179657e8640b9a0c153 GIT binary patch literal 4515 zcmd6pc{J2-|HeOKX38G2l`YkTNko=}kwTUXW6hFL$i6R?HL_(bW>m7oVC=Gtv4#?% zh>>+@GKsMd*<+sL`}}^-`ThI*?|Gg3zw3S8_jS&7o%`I3F$M+T($~?`0YD%SpbO{% z0Amuk34ocHjt&?MhJe8kC^Hlafk0W0A3w&-&dSct#>&RV!O6qL!O6|Z#>U0Z#eI^O zkB^W2gn%GFuOJUEAMeo;5Euf1LZB>AC<`wK8wcb0C z1cU()zyv&n|G{xPylbbgaNTh(;&IuVhzAG&CK#or3=_H!S@&Kk;8nLl#!G?x{M z>^Du^mRm<5YFkp%V!s9Cdag1@B5P*6X6vR{c4S<|MLA1E&An{ zf4H{B8g)jZ#DoEGBE;X(?qX;fC-R$|BFB;G^q97c6!*{g>06!GvQ#^jL@w4;oTCbP zUyXCJzSX5P?JGR`f^(C5m~zl7#F^26Do~yj^1hn5rB|@A5aeFeVmaZ@sU^^OlWWy@ zEMVLRr<=HpEnDjHJ#D%0KRo$NpNnsEHRWYqPLGOKq ztBbd}ZM$3T#Op*X|D$F8TgzJyi7)%L&3%otcKksyJ-aGP=W9wMe!=|iMVrasO_Nza zsbS=u6>2!AqKuL6=}`efK<}1i#(Kaas- zcKh~`Jo18xlB`tPA{|v^(O5RkQrv^GQqhDlo1}1MR z&VOB(`8njdE+7v?MqTG~iRx5>aP`2Rqp%*e@LqUthLaW4%JN+;{7V9nC@6JysUVp( z^IBPG;%)E6=7@@CE!(GG_F*r=rdo)5pD{s5mY{kO=4~UksJrF(b=1nMBTwKQdhX<1 zf!{J=@Z&dBR!ft`PZMb=ssb! ztU!*$=x;83h+%cdUL!c-n5)>Fb2);sotgE8QlCPadW{ESO!WrDE!l` zL2eyLSjd=)Vrfe9?xYrcYVSIU0l><-H!IS;Fm;ph8Ut8@mkU{wWYqDpR-^O2@15;F zA&Cuv*Ea;y0v7tST3b$=B;N7uU#j%jP#<_Bo_%L7#Xr=GUOCUP3=v`xoRl6niSN-v z&>on-&M&lE@04e)Nt?F9K6r~$tP+KfDHAs)BPyP~+s-7e@+mNY$%^;&XM(&~Wbe~L zF(Gl!IuPH7b|tsIJo(?aDw8(^5Loa(agEQfZVDi6C#$+Q?>7wlr))AS z#(gW491uRZJu|a;FnQ~ep7MK%3Zg5b{`2mFb5C-Q!llXvhvLB5h3xulPgGfm+1hYM z*N@%Ztc2G@6Ymv8aR;?Mkx2@kKl4dFl_vTvKrPj6=e?3|9<%Yi}e8i&zi& z7K-E$Lvww_L0_8%^_OdjH&P26Nxk+3^-H!b;9UUDI`Ys0S+rxOiB5DHJD zth~Gosd!1O*TSIrdEAY%)Jv1IjW^UaoaH9=)H2_dcrphnE9>X@GRv6JBw9&E!7HTj zdRy}A`#N8-WG`ADr-%uMRIw73AfawK%mV4H>(e$OTwHz;4;yn=Bq6LXX=ueN~5 zCpE8N9Phx`#}QP`5b*+yVREn;GRpt(%GM9pi<{;LaP6m5`a6sxUKn@o?JL0- zjt2SEW>+DL^$y27`A(!B?X>fy6L(m&C?#j^dLsnTZnoZQ53Y1elrJO(=J%on-fBSF&W2`JA_6?Muj; z?rR?zz`DBB!$lGHktW#1y@Q~)W&J0|O>7(tE1j|vp^L)-bMO=|BgABLcCKrgx`mmU z%T2?^WVk^(Qh^K;e(2w8W4E1FchCQ^wW62!Y2LRRtgN>9oIRt5H|&(ZSo}Wm`e(MK zCt4SyWQOguZ_&W!A7WCAQ3wO8E$6t?P6g_L7uiS4_fswzh@wrVOaCVP5*6kI}1wV(kr%t~5uS(}YAr zmGzDem38diaNS}U8re|OTu^3BHVs%X&7f&lv>hlFdcqpwkf`5R9BbVW=b}!wP{OR{ zPiGa{^MeZ&o=^=qbjvDq=={-)pTd3^OPhDZ;bQN_-jloqJ^s zTW{{+S*XJ@Y@qg1+8M!@q%oB`{rH--HCdnouTuBd=qdv^8J-dB?Udd0{wg(K&tW9| z)Ue-|Z%@62m(~bEQ$fl~r5~n8%+%Lbr1n;Sb`H^Te6uNyWm2TFl4dqn51+c!3!eUX z7;!JN#SZVG;76oMO#kSbQ(u;ABMGow^)}L~tZP-eX#5lE{5ggoYg??mg8!NmDE348 zmTPr~%n2jScbH1Iz^*)>-G_WO{ZkDIp){xEIWMEEuN^Baw3gFF-;--%^!9^?CRGC3 zszry2e#iw}^}J08qdu$63`h&=vSR?Mp;P#R^vWfl-r0fNi0*@h4BD}( zskN3Rj(8L0?=aq5Cb}HQwNl8y6P{-Ksh#f^>hbI*KhrQ15|>}Fgw~mhd=~fK#*7npHsX&3hRE) zuZDa~X5R5_`zOWf9KMBqLc!s;9|`Q+ZeD7t#O#N9ySoQAa8;w@&3BFRMDu?8L=cAe zxM?qrK3(8!-+X%M{Gm`tE|NTtkNHr+pHb$H^h*eote>H`s?%3~{zSa0Sd@xWt~1mc z*IqQkyPFsX@uaAHCq?Y&Z*Jyq4SPAPn>qbyz*xfuQ>&I5vE*v&Gl2R z=sWeq!p8=55OccprM;pti#${HSr>!(2QD-s&lJ*Re&SfMvbt_d_*UuOneGyS#A!lH z?6K#{>UzyPir(U>CY6V5HecT< z@X_DD)}_xSyY$U_bW4&_{OvA^^ajHIXhv39hrX))(2x^2CHB!8|Gjn|PO8u84q8xRJquybx+$48^0oWG)xyhlwt zi!w1(#Pk3z=7SYwYQ32QnpFr6t3I+U_u>;Z6(3~3-Ooo!S13*et-XkN&^n27si3v< zwmbz|J0-Z2S_OnUP&i=AcGp-9o+O7@69lwgZ?&IznhPvw41N96^UC?t--7iYQacvN zXyEt8483PLMnz-8G;yD`FOjBLpDjVsIk9xj82ow*Y@wQ`7l4W*TiBw9F^xzp|9)jO zf?7ne=XBd)LkoUV3mryKg(#OkObI?NKIlfaw+oZr_4OlFY*e#iwgrEHyu=jLhQq0N ziaqCp1~%WMtEBE&3}Qx1fRwRkhBjml>o#m}7w%QF>ruxPS0-@nCn99X^-X4Pc3&NI z8+4rUH~%z=#^Gx>QTs_RW)5A8ss$)|B7&ebE{cfxo^@jW?LJXZ&%Rr2n7s28fPnc% z818$vUeKk7dF6~%ac^Beksb$BIIK9#C@nt{bN58)X*-GC01Dy0g;2rr^S1xIME{*= HFvkA{-goE5 literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.binding.nobohub/pom.xml b/bundles/org.openhab.binding.nobohub/pom.xml new file mode 100644 index 00000000000..0aefabdcd43 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.4.0-SNAPSHOT + + + org.openhab.binding.nobohub + + openHAB Add-ons :: Bundles :: NoboHub Binding + + diff --git a/bundles/org.openhab.binding.nobohub/src/main/feature/feature.xml b/bundles/org.openhab.binding.nobohub/src/main/feature/feature.xml new file mode 100644 index 00000000000..45f55ca040c --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.nobohub/${project.version} + + diff --git a/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/ComponentConfiguration.java b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/ComponentConfiguration.java new file mode 100644 index 00000000000..b2625588c94 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/ComponentConfiguration.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link ComponentConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class ComponentConfiguration { + + /** + * Serial number of the component. + */ + @Nullable + public String serialNumber; +} diff --git a/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/ComponentHandler.java b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/ComponentHandler.java new file mode 100644 index 00000000000..cde394b034c --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/ComponentHandler.java @@ -0,0 +1,159 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal; + +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.CHANNEL_COMPONENT_CURRENT_TEMPERATURE; +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.PROPERTY_MODEL; +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.PROPERTY_NAME; +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.PROPERTY_TEMPERATURE_SENSOR_FOR_ZONE; +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.PROPERTY_ZONE; + +import java.util.Map; + +import javax.measure.quantity.Temperature; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.nobohub.internal.model.Component; +import org.openhab.binding.nobohub.internal.model.SerialNumber; +import org.openhab.binding.nobohub.internal.model.Zone; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.thing.Bridge; +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.types.Command; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Shows information about a Component in the Nobø Hub. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class ComponentHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(ComponentHandler.class); + + private final NoboHubTranslationProvider messages; + + protected @Nullable SerialNumber serialNumber; + + public ComponentHandler(Thing thing, NoboHubTranslationProvider messages) { + super(thing); + this.messages = messages; + } + + public void onUpdate(Component component) { + updateStatus(ThingStatus.ONLINE); + + double temp = component.getTemperature(); + if (!Double.isNaN(temp)) { + QuantityType currentTemperature = new QuantityType<>(temp, SIUnits.CELSIUS); + updateState(CHANNEL_COMPONENT_CURRENT_TEMPERATURE, currentTemperature); + } + + Map properties = editProperties(); + properties.put(Thing.PROPERTY_SERIAL_NUMBER, component.getSerialNumber().toString()); + properties.put(PROPERTY_NAME, component.getName()); + properties.put(PROPERTY_MODEL, component.getSerialNumber().getComponentType()); + + String zoneName = getZoneName(component.getZoneId()); + if (zoneName != null) { + properties.put(PROPERTY_ZONE, zoneName); + } + + String tempForZoneName = getZoneName(component.getTemperatureSensorForZoneId()); + if (tempForZoneName != null) { + properties.put(PROPERTY_TEMPERATURE_SENSOR_FOR_ZONE, tempForZoneName); + } + updateProperties(properties); + } + + private @Nullable String getZoneName(int zoneId) { + Bridge noboHub = getBridge(); + if (null != noboHub) { + NoboHubBridgeHandler hubHandler = (NoboHubBridgeHandler) noboHub.getHandler(); + if (hubHandler != null) { + Zone zone = hubHandler.getZone(zoneId); + if (null != zone) { + return zone.getName(); + } + } + } + + return null; + } + + @Override + public void initialize() { + String serialNumberString = getConfigAs(ComponentConfiguration.class).serialNumber; + if (serialNumberString != null && !serialNumberString.isEmpty()) { + SerialNumber sn = new SerialNumber(serialNumberString); + if (!sn.isWellFormed()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/message.component.illegal.serial [\"" + serialNumberString + "\"]"); + } else { + this.serialNumber = sn; + updateStatus(ThingStatus.ONLINE); + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/message.missing.serial"); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType) { + logger.debug("Refreshing channel {}", channelUID); + if (null != serialNumber) { + Component component = getComponent(); + if (null == component) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, + messages.getText("message.component.notfound", serialNumber, channelUID)); + } else { + onUpdate(component); + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, + "@text/message.component.missing.id [\"" + channelUID + "\"]"); + } + + return; + } + + logger.debug("This component is a read-only device and cannot handle commands."); + } + + public @Nullable SerialNumber getSerialNumber() { + return serialNumber; + } + + private @Nullable Component getComponent() { + Bridge noboHub = getBridge(); + if (null != noboHub) { + NoboHubBridgeHandler hubHandler = (NoboHubBridgeHandler) noboHub.getHandler(); + SerialNumber serialNumber = this.serialNumber; + if (null != serialNumber && null != hubHandler) { + return hubHandler.getComponent(serialNumber); + } + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/Helpers.java b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/Helpers.java new file mode 100644 index 00000000000..30ea68baad8 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/Helpers.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal; + +import java.time.Duration; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Shows information about a Component in the Nobø Hub. + * + * @author Jørgen Austvik - Initial contribution + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +public class Helpers { + + public static String formatDuration(Duration duration) { + long seconds = duration.getSeconds(); + long absSeconds = Math.abs(seconds); + String positive = String.format("%d:%02d:%02d", absSeconds / 3600, (absSeconds % 3600) / 60, absSeconds % 60); + return seconds < 0 ? "-" + positive : positive; + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/NoboHubBindingConstants.java b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/NoboHubBindingConstants.java new file mode 100644 index 00000000000..712598bd6e4 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/NoboHubBindingConstants.java @@ -0,0 +1,116 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal; + +import java.time.Duration; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link NoboHubBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Jørgen Austvik - Initial contribution + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +public class NoboHubBindingConstants { + + private static final String BINDING_ID = "nobohub"; + + public static final String API_VERSION = "1.1"; + + public static final String PROPERTY_NAME = "name"; + public static final String PROPERTY_MODEL = "model"; + public static final String PROPERTY_HOSTNAME = "hostName"; + + public static final String PROPERTY_VENDOR_NAME = "Glen Dimplex Nobø"; + public static final String PROPERTY_PRODUCTION_DATE = "productionDate"; + + public static final String PROPERTY_SOFTWARE_VERSION = "softwareVersion"; + + public static final String PROPERTY_ZONE = "zone"; + public static final String PROPERTY_ZONE_ID = "id"; + public static final String PROPERTY_TEMPERATURE_SENSOR_FOR_ZONE = "temperatureSensorForZone"; + + public static final int NOBO_HUB_TCP_PORT = 27779; + + public static final Duration TIME_BETWEEN_FULL_SCANS = Duration.ofMinutes(10); + public static final Duration TIME_BETWEEN_RETRIES_ON_ERROR = Duration.ofSeconds(10); + + public static final Duration RECOMMENDED_KEEPALIVE_INTERVAL = Duration.ofSeconds(14); + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_HUB = new ThingTypeUID(BINDING_ID, "nobohub"); + public static final ThingTypeUID THING_TYPE_ZONE = new ThingTypeUID(BINDING_ID, "zone"); + public static final ThingTypeUID THING_TYPE_COMPONENT = new ThingTypeUID(BINDING_ID, "component"); + + public static final Set AUTODISCOVERED_THING_TYPES_UIDS = new HashSet<>( + Arrays.asList(THING_TYPE_ZONE, THING_TYPE_COMPONENT)); + + public static final Set SUPPORTED_THING_TYPES_UIDS = new HashSet<>( + Arrays.asList(THING_TYPE_HUB, THING_TYPE_ZONE, THING_TYPE_COMPONENT)); + + // List of all Channel ids + + // Hub + public static final String CHANNEL_HUB_ACTIVE_OVERRIDE_NAME = "activeOverrideName"; + + // Zone + public static final String CHANNEL_ZONE_ACTIVE_WEEK_PROFILE_NAME = "activeWeekProfileName"; + public static final String CHANNEL_ZONE_ACTIVE_WEEK_PROFILE = "activeWeekProfile"; + public static final String CHANNEL_ZONE_CALCULATED_WEEK_PROFILE_STATUS = "calculatedWeekProfileStatus"; + public static final String CHANNEL_ZONE_COMFORT_TEMPERATURE = "comfortTemperature"; + public static final String CHANNEL_ZONE_ECO_TEMPERATURE = "ecoTemperature"; + public static final String CHANNEL_ZONE_CURRENT_TEMPERATURE = "currentTemperature"; + + // Component + public static final String CHANNEL_COMPONENT_CURRENT_TEMPERATURE = "currentTemperature"; + + // Date/time + public static final DateTimeFormatter DATE_FORMAT_SECONDS = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); + public static final DateTimeFormatter DATE_FORMAT_MINUTES = DateTimeFormatter.ofPattern("yyyyMMddHHmm"); + public static final DateTimeFormatter TIME_FORMAT_MINUTES = DateTimeFormatter.ofPattern("HHmm"); + + // Discovery + public static final int NOBO_HUB_BROADCAST_PORT = 10000; + public static final String NOBO_HUB_BROADCAST_ADDRESS = "0.0.0.0"; + public static final int NOBO_HUB_MULTICAST_PORT = 10001; + public static final String NOBO_HUB_MULTICAST_ADDRESS = "239.0.1.187"; + + // Mappings + + public static final Map REJECT_REASONS = Stream.of(new String[][] { + { "0", "Client command set too old. Please run with debug logs." }, + { "1", "Hub serial number mismatch. Should be 12 digits, if hub was autodetected, please add the last three." }, + { "2", "Wrong number of arguments. Please run with debug logs." }, + { "3", "Timestamp incorrectly formatted. Please run with debug logs." }, }) + .collect(Collectors.collectingAndThen(Collectors.toMap(data -> data[0], data -> data[1]), + Collections:: unmodifiableMap)); + + // Full list of units: https://help.nobo.no/skriver/?chapterid=344&chapterlanguageid=2 + public static final Map SERIALNUMBERS_FOR_TYPES = Stream + .of(new String[][] { { "120", "RS-700" }, { "168", "NCU-2R" }, { "184", "NCU-1R" }, { "186", "NTD-4R" }, + { "192", "TXF" }, { "198", "NCU-ER" }, { "210", "NTB-2R" }, { "234", "Nobø Switch" }, }) + .collect(Collectors.collectingAndThen(Collectors.toMap(data -> data[0], data -> data[1]), + Collections:: unmodifiableMap)); +} diff --git a/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/NoboHubBridgeConfiguration.java b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/NoboHubBridgeConfiguration.java new file mode 100644 index 00000000000..43c16e314e3 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/NoboHubBridgeConfiguration.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link NoboHubBridgeConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class NoboHubBridgeConfiguration { + + /** + * Serial number of Nobø Hub. + */ + @Nullable + public String serialNumber; + + /** + * Host address of Nobø Hub. + */ + @Nullable + public String hostName; + + /** + * Polling interval (seconds) + */ + public int pollingInterval; +} diff --git a/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/NoboHubBridgeHandler.java b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/NoboHubBridgeHandler.java new file mode 100644 index 00000000000..309a6bbdde5 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/NoboHubBridgeHandler.java @@ -0,0 +1,418 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal; + +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.CHANNEL_HUB_ACTIVE_OVERRIDE_NAME; +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.PROPERTY_HOSTNAME; +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.PROPERTY_PRODUCTION_DATE; +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.PROPERTY_SOFTWARE_VERSION; +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.RECOMMENDED_KEEPALIVE_INTERVAL; + +import java.time.Duration; +import java.util.Collection; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.nobohub.internal.connection.HubCommunicationThread; +import org.openhab.binding.nobohub.internal.connection.HubConnection; +import org.openhab.binding.nobohub.internal.discovery.NoboThingDiscoveryService; +import org.openhab.binding.nobohub.internal.model.Component; +import org.openhab.binding.nobohub.internal.model.ComponentRegister; +import org.openhab.binding.nobohub.internal.model.Hub; +import org.openhab.binding.nobohub.internal.model.NoboCommunicationException; +import org.openhab.binding.nobohub.internal.model.NoboDataException; +import org.openhab.binding.nobohub.internal.model.OverrideMode; +import org.openhab.binding.nobohub.internal.model.OverridePlan; +import org.openhab.binding.nobohub.internal.model.OverrideRegister; +import org.openhab.binding.nobohub.internal.model.SerialNumber; +import org.openhab.binding.nobohub.internal.model.Temperature; +import org.openhab.binding.nobohub.internal.model.WeekProfile; +import org.openhab.binding.nobohub.internal.model.WeekProfileRegister; +import org.openhab.binding.nobohub.internal.model.Zone; +import org.openhab.binding.nobohub.internal.model.ZoneRegister; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.Bridge; +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.BaseBridgeHandler; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link NoboHubBridgeHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Jørgen Austvik - Initial contribution + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +public class NoboHubBridgeHandler extends BaseBridgeHandler { + + private final Logger logger = LoggerFactory.getLogger(NoboHubBridgeHandler.class); + private @Nullable HubCommunicationThread hubThread; + private @Nullable NoboThingDiscoveryService discoveryService; + private @Nullable Hub hub; + + private final OverrideRegister overrideRegister = new OverrideRegister(); + private final WeekProfileRegister weekProfileRegister = new WeekProfileRegister(); + private final ZoneRegister zoneRegister = new ZoneRegister(); + private final ComponentRegister componentRegister = new ComponentRegister(); + + public NoboHubBridgeHandler(Bridge bridge) { + super(bridge); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + logger.info("Handle command {} for channel {}!", command.toFullString(), channelUID); + + HubCommunicationThread ht = this.hubThread; + Hub h = this.hub; + if (command instanceof RefreshType) { + try { + if (ht != null) { + ht.getConnection().refreshAll(); + } + } catch (NoboCommunicationException noboEx) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/message.bridge.status.failed [\"" + noboEx.getMessage() + "\"]"); + } + + return; + } + + if (CHANNEL_HUB_ACTIVE_OVERRIDE_NAME.equals(channelUID.getId())) { + if (ht != null && h != null) { + if (command instanceof StringType) { + StringType strCommand = (StringType) command; + logger.debug("Changing override for hub {} to {}", channelUID, strCommand); + try { + OverrideMode mode = OverrideMode.getByName(strCommand.toFullString()); + ht.getConnection().setOverride(h, mode); + } catch (NoboCommunicationException nce) { + logger.debug("Failed setting override mode", nce); + } catch (NoboDataException nde) { + logger.debug("Date format error setting override mode", nde); + } + } else { + logger.debug("Command of wrong type: {} ({})", command, command.getClass().getName()); + } + } else { + if (null == h) { + logger.debug("Could not set override, hub not detected yet"); + } + + if (null == ht) { + logger.debug("Could not set override, hub connection thread not set up yet"); + } + } + } + } + + @Override + public void initialize() { + NoboHubBridgeConfiguration config = getConfigAs(NoboHubBridgeConfiguration.class); + + String serialNumber = config.serialNumber; + if (null == serialNumber) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/message.missing.serial"); + return; + } + + String hostName = config.hostName; + if (null == hostName || hostName.isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/message.bridge.missing.hostname"); + return; + } + + logger.debug("Looking for Hub {} at {}", config.serialNumber, config.hostName); + + // Set the thing status to UNKNOWN temporarily and let the background task decide for the real status. + updateStatus(ThingStatus.UNKNOWN); + + // Background handshake: + scheduler.execute(() -> { + try { + HubConnection conn = new HubConnection(hostName, serialNumber, this); + conn.connect(); + + logger.debug("Done connecting to {} ({})", hostName, serialNumber); + + Duration timeout = RECOMMENDED_KEEPALIVE_INTERVAL; + if (config.pollingInterval > 0) { + timeout = Duration.ofSeconds(config.pollingInterval); + } + + logger.debug("Starting communication thread to {}", hostName); + + HubCommunicationThread ht = new HubCommunicationThread(conn, this, timeout); + ht.start(); + hubThread = ht; + + if (ht.getConnection().isConnected()) { + logger.debug("Communication thread to {} is up and running, we are online", hostName); + updateProperty(Thing.PROPERTY_SERIAL_NUMBER, serialNumber); + updateStatus(ThingStatus.ONLINE); + } else { + logger.debug("HubCommunicationThread is not connected anymore, setting to OFFLINE"); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/message.bridge.connection.failed"); + } + } catch (NoboCommunicationException commEx) { + logger.debug("HubCommunicationThread failed, exiting thread"); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, commEx.getMessage()); + } + }); + } + + @Override + public void dispose() { + logger.debug("Disposing NoboHub '{}'", getThing().getUID().getId()); + + final NoboThingDiscoveryService discoveryService = this.discoveryService; + if (discoveryService != null) { + discoveryService.stopScan(); + } + + HubCommunicationThread ht = this.hubThread; + if (ht != null) { + logger.debug("Stopping communication thread"); + ht.stopNow(); + } + } + + @Override + public void childHandlerInitialized(ThingHandler handler, Thing thing) { + logger.info("Adding thing: {}", thing.getLabel()); + } + + @Override + public void childHandlerDisposed(ThingHandler handler, Thing thing) { + logger.info("Disposing thing: {}", thing.getLabel()); + } + + private void onUpdate(Hub hub) { + logger.debug("Updating Hub: {}", hub.getName()); + this.hub = hub; + OverridePlan activeOverridePlan = getOverride(hub.getActiveOverrideId()); + + if (null != activeOverridePlan) { + logger.debug("Updating Hub with ActiveOverrideId {} with Name {}", activeOverridePlan.getId(), + activeOverridePlan.getMode().name()); + + updateState(NoboHubBindingConstants.CHANNEL_HUB_ACTIVE_OVERRIDE_NAME, + StringType.valueOf(activeOverridePlan.getMode().name())); + } + + // Update all zones to set online status and update profile name from weekProfileRegister + for (Zone zone : zoneRegister.values()) { + refreshZone(zone); + } + + Map properties = editProperties(); + properties.put(PROPERTY_HOSTNAME, hub.getName()); + properties.put(Thing.PROPERTY_SERIAL_NUMBER, hub.getSerialNumber().toString()); + properties.put(PROPERTY_SOFTWARE_VERSION, hub.getSoftwareVersion()); + properties.put(Thing.PROPERTY_HARDWARE_VERSION, hub.getHardwareVersion()); + properties.put(PROPERTY_PRODUCTION_DATE, hub.getProductionDate()); + updateProperties(properties); + } + + public void receivedData(@Nullable String line) { + try { + parseLine(line); + } catch (NoboDataException nde) { + logger.debug("Failed parsing line '{}': {}", line, nde.getMessage()); + } + } + + private void parseLine(@Nullable String line) throws NoboDataException { + if (null == line) { + return; + } + + NoboThingDiscoveryService ds = this.discoveryService; + if (line.startsWith("H01")) { + Zone zone = Zone.fromH01(line); + zoneRegister.put(zone); + if (null != ds) { + ds.detectZones(zoneRegister.values()); + } + } else if (line.startsWith("H02")) { + Component component = Component.fromH02(line); + componentRegister.put(component); + if (null != ds) { + ds.detectComponents(componentRegister.values()); + } + } else if (line.startsWith("H03")) { + WeekProfile weekProfile = WeekProfile.fromH03(line); + weekProfileRegister.put(weekProfile); + } else if (line.startsWith("H04")) { + OverridePlan overridePlan = OverridePlan.fromH04(line); + overrideRegister.put(overridePlan); + } else if (line.startsWith("H05")) { + Hub hub = Hub.fromH05(line); + onUpdate(hub); + } else if (line.startsWith("S00")) { + Zone zone = Zone.fromH01(line); + zoneRegister.remove(zone.getId()); + } else if (line.startsWith("S01")) { + Component component = Component.fromH02(line); + componentRegister.remove(component.getSerialNumber()); + } else if (line.startsWith("S02")) { + WeekProfile weekProfile = WeekProfile.fromH03(line); + weekProfileRegister.remove(weekProfile.getId()); + } else if (line.startsWith("S03")) { + OverridePlan overridePlan = OverridePlan.fromH04(line); + overrideRegister.remove(overridePlan.getId()); + } else if (line.startsWith("B00")) { + Zone zone = Zone.fromH01(line); + zoneRegister.put(zone); + if (null != ds) { + ds.detectZones(zoneRegister.values()); + } + } else if (line.startsWith("B01")) { + Component component = Component.fromH02(line); + componentRegister.put(component); + if (null != ds) { + ds.detectComponents(componentRegister.values()); + } + } else if (line.startsWith("B02")) { + WeekProfile weekProfile = WeekProfile.fromH03(line); + weekProfileRegister.put(weekProfile); + } else if (line.startsWith("B03")) { + OverridePlan overridePlan = OverridePlan.fromH04(line); + overrideRegister.put(overridePlan); + } else if (line.startsWith("V00")) { + Zone zone = Zone.fromH01(line); + zoneRegister.put(zone); + refreshZone(zone); + } else if (line.startsWith("V01")) { + Component component = Component.fromH02(line); + componentRegister.put(component); + refreshComponent(component); + } else if (line.startsWith("V02")) { + WeekProfile weekProfile = WeekProfile.fromH03(line); + weekProfileRegister.put(weekProfile); + } else if (line.startsWith("V03")) { + Hub hub = Hub.fromH05(line); + onUpdate(hub); + } else if (line.startsWith("Y02")) { + Temperature temp = Temperature.fromY02(line); + Component component = getComponent(temp.getSerialNumber()); + if (null != component) { + component.setTemperature(temp.getTemperature()); + refreshComponent(component); + int zoneId = component.getTemperatureSensorForZoneId(); + if (zoneId >= 0) { + Zone zone = getZone(zoneId); + if (null != zone) { + zone.setTemperature(temp.getTemperature()); + refreshZone(zone); + } + } + } + } else if (line.startsWith("E00")) { + logger.debug("Error from Hub: {}", line); + } else { + // HANDSHAKE: Basic part of keepalive + // V06: Encryption key + // H00: contains no information + if (!line.startsWith("HANDSHAKE") && !line.startsWith("V06") && !line.startsWith("H00")) { + logger.info("Unknown information from Hub: '{}}'", line); + } + } + } + + public @Nullable Zone getZone(Integer id) { + return zoneRegister.get(id); + } + + public @Nullable WeekProfile getWeekProfile(Integer id) { + return weekProfileRegister.get(id); + } + + public @Nullable Component getComponent(SerialNumber serialNumber) { + return componentRegister.get(serialNumber); + } + + public @Nullable OverridePlan getOverride(Integer id) { + return overrideRegister.get(id); + } + + public void sendCommand(String command) { + @Nullable + HubCommunicationThread ht = this.hubThread; + if (ht != null) { + HubConnection conn = ht.getConnection(); + conn.sendCommand(command); + } + } + + private void refreshZone(Zone zone) { + this.getThing().getThings().forEach(thing -> { + if (thing.getHandler() instanceof ZoneHandler) { + ZoneHandler handler = (ZoneHandler) thing.getHandler(); + if (handler != null && handler.getZoneId() == zone.getId()) { + handler.onUpdate(zone); + } + } + }); + } + + private void refreshComponent(Component component) { + this.getThing().getThings().forEach(thing -> { + if (thing.getHandler() instanceof ComponentHandler) { + ComponentHandler handler = (ComponentHandler) thing.getHandler(); + if (handler != null) { + SerialNumber handlerSerial = handler.getSerialNumber(); + if (handlerSerial != null && component.getSerialNumber().equals(handlerSerial)) { + handler.onUpdate(component); + } + } + } + }); + } + + public void startScan() { + try { + @Nullable + HubCommunicationThread ht = this.hubThread; + if (ht != null) { + ht.getConnection().refreshAll(); + } + } catch (NoboCommunicationException noboEx) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/message.bridge.status.failed [\"" + noboEx.getMessage() + "\"]"); + } + } + + public void setDicsoveryService(NoboThingDiscoveryService discoveryService) { + this.discoveryService = discoveryService; + } + + public Collection getWeekProfiles() { + return weekProfileRegister.values(); + } + + public void setStatusInfo(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) { + updateStatus(status, statusDetail, description); + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/NoboHubConfiguration.java b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/NoboHubConfiguration.java new file mode 100644 index 00000000000..b5126b9e548 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/NoboHubConfiguration.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link NoboHubConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Jørgen Austvik - Initial contribution + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +public class NoboHubConfiguration { + + /** + * Serial number of Nobø Hub. + */ + @Nullable + public String serialNumber; + + /** + * Host address of Nobø Hub. + */ + @Nullable + public String hostName; + + /** + * Polling interval (seconds) + */ + public int pollingInterval; +} diff --git a/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/NoboHubHandlerFactory.java b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/NoboHubHandlerFactory.java new file mode 100644 index 00000000000..f5baaa233b5 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/NoboHubHandlerFactory.java @@ -0,0 +1,132 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal; + +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.SUPPORTED_THING_TYPES_UIDS; +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.THING_TYPE_COMPONENT; +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.THING_TYPE_HUB; +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.THING_TYPE_ZONE; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.nobohub.internal.discovery.NoboThingDiscoveryService; +import org.openhab.core.config.discovery.DiscoveryService; +import org.openhab.core.thing.Bridge; +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.framework.ServiceRegistration; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link NoboHubHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Jørgen Austvik - Initial contribution + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.nobohub", service = ThingHandlerFactory.class) +public class NoboHubHandlerFactory extends BaseThingHandlerFactory { + + private final Logger logger = LoggerFactory.getLogger(NoboHubHandlerFactory.class); + private final Map> discoveryServiceRegs = new HashMap<>(); + public static final Set DISCOVERABLE_DEVICE_TYPES_UIDS = new HashSet<>(List.of(THING_TYPE_HUB)); + private @NonNullByDefault({}) WeekProfileStateDescriptionOptionsProvider stateDescriptionOptionsProvider; + + private final NoboHubTranslationProvider i18nProvider; + + @Activate + public NoboHubHandlerFactory( + final @Reference WeekProfileStateDescriptionOptionsProvider stateDescriptionOptionsProvider, + final @Reference NoboHubTranslationProvider i18nProvider) { + this.stateDescriptionOptionsProvider = stateDescriptionOptionsProvider; + this.i18nProvider = i18nProvider; + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_HUB.equals(thingTypeUID)) { + NoboHubBridgeHandler handler = new NoboHubBridgeHandler((Bridge) thing); + registerDiscoveryService(handler); + return handler; + } else if (THING_TYPE_ZONE.equals(thingTypeUID)) { + logger.debug("Setting WeekProfileStateDescriptionOptionsProvider for: {}", thing.getLabel()); + return new ZoneHandler(thing, i18nProvider, stateDescriptionOptionsProvider); + } else if (THING_TYPE_COMPONENT.equals(thingTypeUID)) { + return new ComponentHandler(thing, i18nProvider); + } + + return null; + } + + @Override + protected void removeHandler(ThingHandler thingHandler) { + if (thingHandler instanceof NoboHubBridgeHandler) { + unregisterDiscoveryService((NoboHubBridgeHandler) thingHandler); + } + } + + private synchronized void registerDiscoveryService(NoboHubBridgeHandler bridgeHandler) { + NoboThingDiscoveryService discoveryService = new NoboThingDiscoveryService(bridgeHandler); + bridgeHandler.setDicsoveryService(discoveryService); + this.discoveryServiceRegs.put(bridgeHandler.getThing().getThingTypeUID(), getBundleContext() + .registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>())); + } + + private synchronized void unregisterDiscoveryService(NoboHubBridgeHandler bridgeHandler) { + try { + ServiceRegistration serviceReg = this.discoveryServiceRegs + .remove(bridgeHandler.getThing().getThingTypeUID()); + if (null != serviceReg) { + NoboThingDiscoveryService service = (NoboThingDiscoveryService) getBundleContext() + .getService(serviceReg.getReference()); + serviceReg.unregister(); + if (null != service) { + service.deactivate(); + } + } + } catch (IllegalArgumentException iae) { + logger.debug("Failed to unregister service", iae); + } + } + + @Reference + protected void setDynamicStateDescriptionProvider(WeekProfileStateDescriptionOptionsProvider provider) { + this.stateDescriptionOptionsProvider = provider; + } + + protected void unsetDynamicStateDescriptionProvider(WeekProfileStateDescriptionOptionsProvider provider) { + this.stateDescriptionOptionsProvider = null; + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/NoboHubTranslationProvider.java b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/NoboHubTranslationProvider.java new file mode 100644 index 00000000000..7066e51f59d --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/NoboHubTranslationProvider.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal; + +import java.util.Locale; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TranslationProvider; +import org.osgi.framework.Bundle; +import org.osgi.framework.FrameworkUtil; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * This class provides translated texts + * + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +@Component(service = NoboHubTranslationProvider.class) +public class NoboHubTranslationProvider { + + private final Bundle bundle; + private final TranslationProvider i18nProvider; + private final LocaleProvider localeProvider; + + @Activate + public NoboHubTranslationProvider(@Reference TranslationProvider i18nProvider, + @Reference LocaleProvider localeProvider) { + this.bundle = FrameworkUtil.getBundle(this.getClass()); + this.i18nProvider = i18nProvider; + this.localeProvider = localeProvider; + } + + public NoboHubTranslationProvider(final NoboHubTranslationProvider other) { + this.bundle = other.bundle; + this.i18nProvider = other.i18nProvider; + this.localeProvider = other.localeProvider; + } + + public String getText(String key, @Nullable Object... arguments) { + Locale locale = localeProvider.getLocale(); + String message = i18nProvider.getText(bundle, key, this.getDefaultText(key), locale, arguments); + if (message != null) { + return message; + } + return key; + } + + public @Nullable String getDefaultText(String key) { + return i18nProvider.getText(bundle, key, key, Locale.ENGLISH); + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/WeekProfileStateDescriptionOptionsProvider.java b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/WeekProfileStateDescriptionOptionsProvider.java new file mode 100644 index 00000000000..f2d7a09f743 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/WeekProfileStateDescriptionOptionsProvider.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.events.EventPublisher; +import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider; +import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService; +import org.openhab.core.thing.link.ItemChannelLinkRegistry; +import org.openhab.core.thing.type.DynamicStateDescriptionProvider; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * Dynamic provider of week profile state options. + * + * @author Espen Fossen - Initial contribution + */ +@Component(service = { DynamicStateDescriptionProvider.class, WeekProfileStateDescriptionOptionsProvider.class }) +@NonNullByDefault +public class WeekProfileStateDescriptionOptionsProvider extends BaseDynamicStateDescriptionProvider { + + @Activate + public WeekProfileStateDescriptionOptionsProvider(final @Reference EventPublisher eventPublisher, // + final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry, // + final @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) { + this.eventPublisher = eventPublisher; + this.itemChannelLinkRegistry = itemChannelLinkRegistry; + this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService; + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/ZoneConfiguration.java b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/ZoneConfiguration.java new file mode 100644 index 00000000000..a3a85d146c3 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/ZoneConfiguration.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link ZoneConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class ZoneConfiguration { + + /** + * Id of the zone + */ + public int id; +} diff --git a/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/ZoneHandler.java b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/ZoneHandler.java new file mode 100644 index 00000000000..38eebf6f67c --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/ZoneHandler.java @@ -0,0 +1,235 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal; + +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.CHANNEL_ZONE_ACTIVE_WEEK_PROFILE; +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.CHANNEL_ZONE_ACTIVE_WEEK_PROFILE_NAME; +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.CHANNEL_ZONE_CALCULATED_WEEK_PROFILE_STATUS; +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.CHANNEL_ZONE_COMFORT_TEMPERATURE; +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.CHANNEL_ZONE_CURRENT_TEMPERATURE; +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.CHANNEL_ZONE_ECO_TEMPERATURE; +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.PROPERTY_HOSTNAME; +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.PROPERTY_ZONE_ID; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import javax.measure.quantity.Temperature; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.nobohub.internal.model.NoboDataException; +import org.openhab.binding.nobohub.internal.model.WeekProfile; +import org.openhab.binding.nobohub.internal.model.WeekProfileStatus; +import org.openhab.binding.nobohub.internal.model.Zone; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.thing.Bridge; +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.types.Command; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.StateOption; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Shows information about a named Zone in the Nobø Hub. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class ZoneHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(ZoneHandler.class); + + private final WeekProfileStateDescriptionOptionsProvider weekProfileStateDescriptionOptionsProvider; + + private final NoboHubTranslationProvider messages; + + protected int id; + + public ZoneHandler(Thing thing, NoboHubTranslationProvider messages, + WeekProfileStateDescriptionOptionsProvider weekProfileStateDescriptionOptionsProvider) { + super(thing); + this.messages = messages; + this.weekProfileStateDescriptionOptionsProvider = weekProfileStateDescriptionOptionsProvider; + } + + public void onUpdate(Zone zone) { + logger.debug("Updating zone: {}", zone.getName()); + updateStatus(ThingStatus.ONLINE); + + QuantityType comfortTemperature = new QuantityType<>(zone.getComfortTemperature(), + SIUnits.CELSIUS); + updateState(CHANNEL_ZONE_COMFORT_TEMPERATURE, comfortTemperature); + QuantityType ecoTemperature = new QuantityType<>(zone.getEcoTemperature(), SIUnits.CELSIUS); + updateState(CHANNEL_ZONE_ECO_TEMPERATURE, ecoTemperature); + + Double temp = zone.getTemperature(); + if (temp != null && !Double.isNaN(temp)) { + QuantityType currentTemperature = new QuantityType<>(temp, SIUnits.CELSIUS); + updateState(CHANNEL_ZONE_CURRENT_TEMPERATURE, currentTemperature); + } + + int activeWeekProfileId = zone.getActiveWeekProfileId(); + Bridge noboHub = getBridge(); + if (null != noboHub) { + logger.debug("Updating zone: {} at hub bridge: {}", zone.getName(), + noboHub.getStatusInfo().getStatus().name()); + NoboHubBridgeHandler hubHandler = (NoboHubBridgeHandler) noboHub.getHandler(); + if (hubHandler != null) { + WeekProfile weekProfile = hubHandler.getWeekProfile(activeWeekProfileId); + if (null != weekProfile) { + updateState(CHANNEL_ZONE_ACTIVE_WEEK_PROFILE_NAME, StringType.valueOf(weekProfile.getName())); + updateState(CHANNEL_ZONE_ACTIVE_WEEK_PROFILE, + DecimalType.valueOf(String.valueOf(weekProfile.getId()))); + try { + WeekProfileStatus weekProfileStatus = weekProfile.getStatusAt(LocalDateTime.now()); + updateState(CHANNEL_ZONE_CALCULATED_WEEK_PROFILE_STATUS, + StringType.valueOf(weekProfileStatus.name())); + } catch (NoboDataException nde) { + logger.debug("Failed getting current week profile status", nde); + } + } + + List options = new ArrayList<>(); + logger.debug("Updating week profile state description options for zone {}.", zone.getName()); + for (WeekProfile wp : hubHandler.getWeekProfiles()) { + options.add(new StateOption(String.valueOf(wp.getId()), wp.getName())); + } + logger.debug("State options count: {}. First: {}", options.size(), + (!options.isEmpty()) ? options.get(0) : 0); + weekProfileStateDescriptionOptionsProvider.setStateOptions( + new ChannelUID(getThing().getUID(), CHANNEL_ZONE_ACTIVE_WEEK_PROFILE), options); + } + } + + Map properties = editProperties(); + properties.put(PROPERTY_HOSTNAME, zone.getName()); + properties.put(PROPERTY_ZONE_ID, Integer.toString(zone.getId())); + updateProperties(properties); + } + + @Override + public void initialize() { + this.id = getConfigAs(ZoneConfiguration.class).id; + updateStatus(ThingStatus.ONLINE); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType) { + logger.debug("Refreshing channel {}", channelUID); + + Zone zone = getZone(); + if (null == zone) { + logger.debug("Could not find Zone with id {} for channel {}", id, channelUID); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, + messages.getText("message.zone.notfound", id, channelUID)); + } else { + onUpdate(zone); + Bridge noboHub = getBridge(); + if (null != noboHub) { + NoboHubBridgeHandler hubHandler = (NoboHubBridgeHandler) noboHub.getHandler(); + if (null != hubHandler) { + WeekProfile weekProfile = hubHandler.getWeekProfile(zone.getActiveWeekProfileId()); + if (null != weekProfile) { + String weekProfileName = weekProfile.getName(); + StringType weekProfileValue = StringType.valueOf(weekProfileName); + updateState(CHANNEL_ZONE_ACTIVE_WEEK_PROFILE_NAME, weekProfileValue); + } + } + } + } + + return; + } + + if (CHANNEL_ZONE_COMFORT_TEMPERATURE.equals(channelUID.getId())) { + Zone zone = getZone(); + if (zone != null) { + if (command instanceof DecimalType) { + DecimalType comfortTemp = (DecimalType) command; + logger.debug("Set comfort temp for zone {} to {}", zone.getName(), comfortTemp.doubleValue()); + zone.setComfortTemperature(comfortTemp.intValue()); + sendCommand(zone.generateCommandString("U00")); + } + } + + return; + } + + if (CHANNEL_ZONE_ECO_TEMPERATURE.equals(channelUID.getId())) { + Zone zone = getZone(); + if (zone != null) { + if (command instanceof DecimalType) { + DecimalType ecoTemp = (DecimalType) command; + logger.debug("Set eco temp for zone {} to {}", zone.getName(), ecoTemp.doubleValue()); + zone.setEcoTemperature(ecoTemp.intValue()); + sendCommand(zone.generateCommandString("U00")); + } + } + return; + } + + if (CHANNEL_ZONE_ACTIVE_WEEK_PROFILE.equals(channelUID.getId())) { + Zone zone = getZone(); + if (zone != null) { + if (command instanceof DecimalType) { + DecimalType weekProfileId = (DecimalType) command; + logger.debug("Set week profile for zone {} to {}", zone.getName(), weekProfileId); + zone.setWeekProfile(weekProfileId.intValue()); + sendCommand(zone.generateCommandString("U00")); + } + } + + return; + } + + logger.debug("Unhandled zone command {}: {}", channelUID.getId(), command); + } + + public @Nullable Integer getZoneId() { + return id; + } + + private void sendCommand(String command) { + Bridge noboHub = getBridge(); + if (null != noboHub) { + NoboHubBridgeHandler hubHandler = (NoboHubBridgeHandler) noboHub.getHandler(); + if (null != hubHandler) { + hubHandler.sendCommand(command); + } + } + } + + private @Nullable Zone getZone() { + Bridge noboHub = getBridge(); + if (null != noboHub) { + NoboHubBridgeHandler hubHandler = (NoboHubBridgeHandler) noboHub.getHandler(); + if (null != hubHandler) { + return hubHandler.getZone(id); + } + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/connection/HubCommunicationThread.java b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/connection/HubCommunicationThread.java new file mode 100644 index 00000000000..a562e7891f0 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/connection/HubCommunicationThread.java @@ -0,0 +1,167 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal.connection; + +import java.time.Duration; +import java.time.Instant; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.nobohub.internal.NoboHubBindingConstants; +import org.openhab.binding.nobohub.internal.NoboHubBridgeHandler; +import org.openhab.binding.nobohub.internal.model.NoboCommunicationException; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Thread that reads from the Nobø Hub and sends HANDSHAKEs to keep the connection open. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class HubCommunicationThread extends Thread { + + private enum HubCommunicationThreadState { + STARTING(null, null, ""), + CONNECTED(ThingStatus.ONLINE, ThingStatusDetail.NONE, ""), + DISCONNECTED(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/message.bridge.status.failed"), + STOPPED(null, null, ""); + + private final @Nullable ThingStatus status; + private final @Nullable ThingStatusDetail statusDetail; + private final String errorMessage; + + HubCommunicationThreadState(@Nullable ThingStatus status, @Nullable ThingStatusDetail statusDetail, + String errorMessage) { + this.status = status; + this.statusDetail = statusDetail; + this.errorMessage = errorMessage; + } + + public @Nullable ThingStatus getThingStatus() { + return status; + } + + public @Nullable ThingStatusDetail getThingStatusDetail() { + return statusDetail; + } + + public String getErrorMessage() { + return errorMessage; + } + } + + private final Logger logger = LoggerFactory.getLogger(HubCommunicationThread.class); + + private final HubConnection hubConnection; + private final NoboHubBridgeHandler hubHandler; + private final Duration timeout; + private Instant lastTimeFullScan; + + private volatile boolean stopped = false; + private HubCommunicationThreadState currentState = HubCommunicationThreadState.STARTING; + + public HubCommunicationThread(HubConnection hubConnection, NoboHubBridgeHandler hubHandler, Duration timeout) { + this.hubConnection = hubConnection; + this.hubHandler = hubHandler; + this.timeout = timeout; + this.lastTimeFullScan = Instant.now(); + } + + public void stopNow() { + stopped = true; + } + + @Override + public void run() { + while (!stopped) { + switch (currentState) { + case STARTING: + try { + hubConnection.refreshAll(); + lastTimeFullScan = Instant.now(); + setNextState(HubCommunicationThreadState.CONNECTED); + } catch (NoboCommunicationException nce) { + logger.debug("Communication error with Hub", nce); + setNextState(HubCommunicationThreadState.DISCONNECTED); + } + break; + + case CONNECTED: + try { + if (hubConnection.hasData()) { + hubConnection.processReads(timeout); + } + + if (Instant.now() + .isAfter(lastTimeFullScan.plus(NoboHubBindingConstants.TIME_BETWEEN_FULL_SCANS))) { + hubConnection.refreshAll(); + lastTimeFullScan = Instant.now(); + } else { + hubConnection.handshake(); + } + + hubConnection.processReads(timeout); + } catch (NoboCommunicationException nce) { + logger.debug("Communication error with Hub", nce); + setNextState(HubCommunicationThreadState.DISCONNECTED); + } + break; + + case DISCONNECTED: + try { + Thread.sleep(NoboHubBindingConstants.TIME_BETWEEN_RETRIES_ON_ERROR.toMillis()); + try { + logger.debug("Trying to do a hard reconnect"); + hubConnection.hardReconnect(); + setNextState(HubCommunicationThreadState.CONNECTED); + } catch (NoboCommunicationException nce2) { + logger.debug("Failed to reconnect connection", nce2); + } + } catch (InterruptedException ie) { + logger.debug("Interrupted from sleep after error"); + Thread.currentThread().interrupt(); + } + break; + + case STOPPED: + break; + } + } + + if (stopped) { + logger.debug("HubCommunicationThread is stopped, disconnecting from Hub"); + setNextState(HubCommunicationThreadState.STOPPED); + try { + hubConnection.disconnect(); + } catch (NoboCommunicationException nce) { + logger.debug("Error disconnecting from Hub", nce); + } + } + } + + public HubConnection getConnection() { + return hubConnection; + } + + private void setNextState(HubCommunicationThreadState newState) { + currentState = newState; + ThingStatus stateThingStatus = newState.getThingStatus(); + ThingStatusDetail stateThingStatusDetail = newState.getThingStatusDetail(); + if (null != stateThingStatus && null != stateThingStatusDetail) { + hubHandler.setStatusInfo(stateThingStatus, stateThingStatusDetail, newState.getErrorMessage()); + } + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/connection/HubConnection.java b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/connection/HubConnection.java new file mode 100644 index 00000000000..b0bdcc84568 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/connection/HubConnection.java @@ -0,0 +1,270 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal.connection; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.time.Duration; +import java.time.LocalDateTime; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.nobohub.internal.Helpers; +import org.openhab.binding.nobohub.internal.NoboHubBindingConstants; +import org.openhab.binding.nobohub.internal.NoboHubBridgeHandler; +import org.openhab.binding.nobohub.internal.model.Hub; +import org.openhab.binding.nobohub.internal.model.NoboCommunicationException; +import org.openhab.binding.nobohub.internal.model.NoboDataException; +import org.openhab.binding.nobohub.internal.model.OverrideMode; +import org.openhab.binding.nobohub.internal.model.OverridePlan; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Connection to the Nobø Hub (Socket wrapper). + * + * @author Jørgen Austvik - Initial contribution + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +public class HubConnection { + + private final Logger logger = LoggerFactory.getLogger(HubConnection.class); + + private final String hostName; + private final NoboHubBridgeHandler hubHandler; + private final String serialNumber; + + private @Nullable InetAddress host; + private @Nullable Socket hubConnection; + private @Nullable PrintWriter out; + private @Nullable BufferedReader in; + + public HubConnection(String hostName, String serialNumber, NoboHubBridgeHandler hubHandler) + throws NoboCommunicationException { + this.hostName = hostName; + this.serialNumber = serialNumber; + this.hubHandler = hubHandler; + } + + public void connect() throws NoboCommunicationException { + connectSocket(); + + String hello = String.format("HELLO %s %s %s\r", NoboHubBindingConstants.API_VERSION, serialNumber, + getDateString()); + write(hello); + + String helloRes = readLine(); + if (null == helloRes || !helloRes.startsWith("HELLO")) { + if (helloRes != null && helloRes.startsWith("REJECT")) { + String[] reject = helloRes.split(" ", 2); + throw new NoboCommunicationException(String.format("Hub rejects us with reason %s: %s", reject[1], + NoboHubBindingConstants.REJECT_REASONS.get(reject[1]))); + } else { + throw new NoboCommunicationException("Hub rejects us with unknown reason"); + } + } + + write("HANDSHAKE\r"); + + String handshakeRes = readLine(); + if (null == handshakeRes || !handshakeRes.startsWith("HANDSHAKE")) { + throw new NoboCommunicationException("Hub rejects handshake"); + } + + refreshAllNoReconnect(); + } + + public void handshake() throws NoboCommunicationException { + if (!isConnected()) { + connect(); + } else { + write("HANDSHAKE\r"); + } + } + + public void setOverride(Hub hub, OverrideMode nextMode) throws NoboDataException, NoboCommunicationException { + if (!isConnected()) { + connect(); + } + + OverridePlan overridePlan = OverridePlan.fromMode(nextMode, LocalDateTime.now()); + sendCommand(overridePlan.generateCommandString("A03")); + + String line = ""; + while (line != null && !line.startsWith("B03")) { + line = readLine(); + hubHandler.receivedData(line); + } + + String l = line; + if (null != l) { + OverridePlan newOverridePlan = OverridePlan.fromH04(l); + hub.setActiveOverrideId(newOverridePlan.getId()); + sendCommand(hub.generateCommandString("U03")); + } + } + + public void refreshAll() throws NoboCommunicationException { + if (!isConnected()) { + connect(); + } else { + refreshAllNoReconnect(); + } + } + + private void refreshAllNoReconnect() throws NoboCommunicationException { + write("G00\r"); + + String line = ""; + while (line != null && !line.startsWith("H05")) { + line = readLine(); + hubHandler.receivedData(line); + } + } + + public boolean isConnected() { + Socket conn = this.hubConnection; + if (null != conn) { + return conn.isConnected(); + } + + return false; + } + + public boolean hasData() throws NoboCommunicationException { + BufferedReader i = this.in; + if (null != i) { + try { + return i.ready(); + } catch (IOException ioex) { + throw new NoboCommunicationException("Failed detecting if buffer has any data", ioex); + } + } + + return false; + } + + public void processReads(Duration timeout) throws NoboCommunicationException { + try { + Socket conn = this.hubConnection; + if (null == conn) { + throw new NoboCommunicationException("No connection to Hub"); + } + + logger.trace("Reading from Hub, waiting maximum {}", Helpers.formatDuration(timeout)); + conn.setSoTimeout((int) timeout.toMillis()); + + try { + String line = readLine(); + if (line != null && line.startsWith("HANDSHAKE")) { + line = readLine(); + } + + hubHandler.receivedData(line); + } catch (NoboCommunicationException nce) { + if (!(nce.getCause() instanceof SocketTimeoutException)) { + connectSocket(); + } + } + } catch (SocketException se) { + throw new NoboCommunicationException("Failed setting read timeout", se); + } + } + + private @Nullable String readLine() throws NoboCommunicationException { + BufferedReader reader = this.in; + try { + if (null != reader) { + String line = reader.readLine(); + if (line != null) { + logger.trace("Reading raw data string from Nobø Hub: {}", line); + } + return line; + } + } catch (IOException ioex) { + throw new NoboCommunicationException("Failed reading from Nobø Hub", ioex); + } + + return null; + } + + public void sendCommand(String command) { + write(command); + } + + private void write(String s) { + @Nullable + PrintWriter o = this.out; + if (null != o) { + logger.trace("Sending '{}'", s); + o.write(s); + o.flush(); + } + } + + private void connectSocket() throws NoboCommunicationException { + if (null == host) { + try { + host = InetAddress.getByName(hostName); + } catch (IOException ioex) { + throw new NoboCommunicationException(String.format("Failed to resolve IP address of %s", hostName), + ioex); + } + } + try { + Socket conn = new Socket(host, NoboHubBindingConstants.NOBO_HUB_TCP_PORT); + out = new PrintWriter(conn.getOutputStream(), true); + in = new BufferedReader(new InputStreamReader(conn.getInputStream())); + hubConnection = conn; + } catch (IOException ioex) { + throw new NoboCommunicationException(String.format("Failed connecting to Nobø Hub at %s", hostName), ioex); + } + } + + public void disconnect() throws NoboCommunicationException { + try { + PrintWriter o = this.out; + if (o != null) { + o.close(); + } + + BufferedReader i = this.in; + if (i != null) { + i.close(); + } + + Socket conn = this.hubConnection; + if (conn != null) { + conn.close(); + } + } catch (IOException ioex) { + throw new NoboCommunicationException("Error disconnecting from Hub", ioex); + } + } + + public void hardReconnect() throws NoboCommunicationException { + disconnect(); + connect(); + } + + private String getDateString() { + return LocalDateTime.now().format(NoboHubBindingConstants.DATE_FORMAT_SECONDS); + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/discovery/NoboHubDiscoveryService.java b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/discovery/NoboHubDiscoveryService.java new file mode 100644 index 00000000000..b1999634843 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/discovery/NoboHubDiscoveryService.java @@ -0,0 +1,163 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal.discovery; + +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.NOBO_HUB_BROADCAST_ADDRESS; +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.NOBO_HUB_BROADCAST_PORT; +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.NOBO_HUB_MULTICAST_PORT; +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.PROPERTY_HOSTNAME; +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.PROPERTY_NAME; +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.PROPERTY_VENDOR_NAME; +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.THING_TYPE_HUB; +import static org.openhab.binding.nobohub.internal.NoboHubHandlerFactory.DISCOVERABLE_DEVICE_TYPES_UIDS; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.MulticastSocket; +import java.time.Duration; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.nobohub.internal.NoboHubBridgeHandler; +import org.openhab.core.config.discovery.AbstractDiscoveryService; +import org.openhab.core.config.discovery.DiscoveryResult; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.config.discovery.DiscoveryService; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.osgi.service.component.annotations.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class identifies devices that are available on the Nobø hub and adds discovery results for them. + * + * @author Jørgen Austvik - Initial contribution + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.nobohub") +public class NoboHubDiscoveryService extends AbstractDiscoveryService implements DiscoveryService, ThingHandlerService { + private final Logger logger = LoggerFactory.getLogger(NoboHubDiscoveryService.class); + + private @NonNullByDefault({}) NoboHubBridgeHandler hubBridgeHandler; + + public NoboHubDiscoveryService() { + super(DISCOVERABLE_DEVICE_TYPES_UIDS, 10, true); + } + + @Override + protected void startScan() { + scheduler.execute(scanner); + } + + @Override + protected synchronized void stopScan() { + super.stopScan(); + removeOlderResults(getTimestampOfLastScan()); + } + + @Override + public void deactivate() { + removeOlderResults(new Date().getTime()); + } + + @Override + public void setThingHandler(ThingHandler thingHandler) { + if (thingHandler instanceof NoboHubBridgeHandler) { + this.hubBridgeHandler = (NoboHubBridgeHandler) thingHandler; + } + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return hubBridgeHandler; + } + + private final Runnable scanner = new Runnable() { + @Override + public void run() { + boolean found = false; + logger.info("Detecting Glen Dimplex Nobø Hubs, trying Multicast"); + try { + MulticastSocket socket = new MulticastSocket(NOBO_HUB_MULTICAST_PORT); + found = waitOnSocket(socket, "multicast"); + } catch (IOException ioex) { + logger.error("Failed detecting Nobø Hub via multicast", ioex); + } + + if (!found) { + logger.debug("Detecting Glen Dimplex Nobø Hubs, trying Broadcast"); + + try { + DatagramSocket socket = new DatagramSocket(NOBO_HUB_BROADCAST_PORT, + InetAddress.getByName(NOBO_HUB_BROADCAST_ADDRESS)); + found = waitOnSocket(socket, "broadcast"); + } catch (IOException ioex) { + logger.error("Failed detecting Nobø Hub via multicast, will try with Broadcast", ioex); + } + } + } + + private boolean waitOnSocket(DatagramSocket socket, String type) throws IOException { + try (socket) { + socket.setBroadcast(true); + + byte[] buffer = new byte[1024]; + DatagramPacket data = new DatagramPacket(buffer, buffer.length); + String received = ""; + while (!received.startsWith("__NOBOHUB__")) { + socket.setSoTimeout((int) Duration.ofSeconds(4).toMillis()); + socket.receive(data); + received = new String(buffer, 0, data.getLength()); + } + + logger.debug("Hub detection using {}: Received: {} from {}", type, received, data.getAddress()); + + String[] parts = received.split("__", 3); + if (3 != parts.length) { + logger.debug("Data error, didn't contain three parts: '{}''", String.join("','", parts)); + return false; + } + + String serialNumberStart = parts[parts.length - 1]; + addDevice(serialNumberStart, data.getAddress().getHostName()); + return true; + } + } + + private void addDevice(String serialNumberStart, String hostName) { + ThingUID bridge = new ThingUID(THING_TYPE_HUB, serialNumberStart); + String label = "Nobø Hub " + serialNumberStart; + + Map properties = new HashMap<>(4); + properties.put(Thing.PROPERTY_SERIAL_NUMBER, serialNumberStart); + properties.put(PROPERTY_NAME, label); + properties.put(Thing.PROPERTY_VENDOR, PROPERTY_VENDOR_NAME); + properties.put(PROPERTY_HOSTNAME, hostName); + + logger.debug("Adding device {} to inbox: {} {} at {}", bridge, label, serialNumberStart, hostName); + DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(bridge).withLabel(label) + .withProperties(properties).withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER).build(); + thingDiscovered(discoveryResult); + } + }; +} diff --git a/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/discovery/NoboThingDiscoveryService.java b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/discovery/NoboThingDiscoveryService.java new file mode 100644 index 00000000000..58c27914a27 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/discovery/NoboThingDiscoveryService.java @@ -0,0 +1,161 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal.discovery; + +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.AUTODISCOVERED_THING_TYPES_UIDS; +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.PROPERTY_MODEL; +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.PROPERTY_NAME; +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.PROPERTY_TEMPERATURE_SENSOR_FOR_ZONE; +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.PROPERTY_VENDOR_NAME; +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.PROPERTY_ZONE; +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.PROPERTY_ZONE_ID; +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.THING_TYPE_COMPONENT; +import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.THING_TYPE_ZONE; + +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.nobohub.internal.NoboHubBridgeHandler; +import org.openhab.binding.nobohub.internal.model.Component; +import org.openhab.binding.nobohub.internal.model.Zone; +import org.openhab.core.config.discovery.AbstractDiscoveryService; +import org.openhab.core.config.discovery.DiscoveryResult; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingUID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class identifies devices that are available on the Nobø hub and adds discovery results for them. + * + * @author Jørgen Austvik - Initial contribution + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +public class NoboThingDiscoveryService extends AbstractDiscoveryService { + private final Logger logger = LoggerFactory.getLogger(NoboThingDiscoveryService.class); + + private final NoboHubBridgeHandler bridgeHandler; + + public NoboThingDiscoveryService(NoboHubBridgeHandler bridgeHandler) { + super(AUTODISCOVERED_THING_TYPES_UIDS, 10, true); + this.bridgeHandler = bridgeHandler; + } + + @Override + protected void startScan() { + bridgeHandler.startScan(); + } + + @Override + public synchronized void stopScan() { + super.stopScan(); + removeOlderResults(getTimestampOfLastScan()); + } + + @Override + public void deactivate() { + removeOlderResults(new Date().getTime()); + } + + public void detectZones(Collection zones) { + ThingUID bridge = bridgeHandler.getThing().getUID(); + List things = bridgeHandler.getThing().getThings(); + + for (Zone zone : zones) { + ThingUID discoveredThingId = new ThingUID(THING_TYPE_ZONE, bridge, Integer.toString(zone.getId())); + + boolean addDiscoveredZone = true; + for (Thing thing : things) { + if (thing.getUID().equals(discoveredThingId)) { + addDiscoveredZone = false; + } + } + + if (addDiscoveredZone) { + String label = zone.getName(); + + Map properties = new HashMap<>(3); + properties.put(PROPERTY_ZONE_ID, Integer.toString(zone.getId())); + properties.put(PROPERTY_NAME, zone.getName()); + properties.put(Thing.PROPERTY_VENDOR, PROPERTY_VENDOR_NAME); + + logger.debug("Adding device {} to inbox", discoveredThingId); + DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(discoveredThingId).withBridge(bridge) + .withLabel(label).withProperties(properties).withRepresentationProperty("id").build(); + thingDiscovered(discoveryResult); + } + } + } + + public void detectComponents(Collection components) { + ThingUID bridge = bridgeHandler.getThing().getUID(); + List things = bridgeHandler.getThing().getThings(); + + for (Component component : components) { + ThingUID discoveredThingId = new ThingUID(THING_TYPE_COMPONENT, bridge, + component.getSerialNumber().toString()); + + boolean addDiscoveredComponent = true; + for (Thing thing : things) { + if (thing.getUID().equals(discoveredThingId)) { + addDiscoveredComponent = false; + } + } + + if (addDiscoveredComponent) { + String label = component.getName(); + + Map properties = new HashMap<>(4); + properties.put(Thing.PROPERTY_SERIAL_NUMBER, component.getSerialNumber().toString()); + properties.put(PROPERTY_NAME, component.getName()); + properties.put(Thing.PROPERTY_VENDOR, PROPERTY_VENDOR_NAME); + properties.put(PROPERTY_MODEL, component.getSerialNumber().getComponentType()); + + String zoneName = getZoneName(component.getZoneId()); + if (zoneName != null) { + properties.put(PROPERTY_ZONE, zoneName); + } + + int zoneId = component.getTemperatureSensorForZoneId(); + if (zoneId >= 0) { + String tempForZoneName = getZoneName(zoneId); + if (tempForZoneName != null) { + properties.put(PROPERTY_TEMPERATURE_SENSOR_FOR_ZONE, tempForZoneName); + } + } + + logger.debug("Adding device {} to inbox", discoveredThingId); + DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(discoveredThingId).withBridge(bridge) + .withLabel(label).withProperties(properties) + .withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER).build(); + thingDiscovered(discoveryResult); + } + } + } + + private @Nullable String getZoneName(int zoneId) { + Zone zone = bridgeHandler.getZone(zoneId); + if (null == zone) { + return null; + } + + return zone.getName(); + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/Component.java b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/Component.java new file mode 100644 index 00000000000..ea3be882858 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/Component.java @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal.model; + +import java.util.StringJoiner; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * A Component in the Nobø Hub can be a oven, a floor or a switch. + * + * @author Jørgen Austvik - Initial contribution + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +public final class Component { + + private final SerialNumber serialNumber; + private final String name; + private final boolean reverse; + private final int zoneId; + private final int temperatureSensorForZoneId; + private double temperature; + + public Component(SerialNumber serialNumber, String name, boolean reverse, int zoneId, + int temperatureSensorForZoneId) { + this.serialNumber = serialNumber; + this.name = name; + this.reverse = reverse; + this.zoneId = zoneId; + this.temperatureSensorForZoneId = temperatureSensorForZoneId; + } + + public static Component fromH02(String h02) throws NoboDataException { + String[] parts = h02.split(" ", 8); + + if (parts.length != 8) { + throw new NoboDataException( + String.format("Unexpected number of parts from hub on H2 call: %d", parts.length)); + } + + SerialNumber serial = new SerialNumber(ModelHelper.toJavaString(parts[1])); + if (!serial.isWellFormed()) { + throw new NoboDataException(String.format("Illegal serial number: '%s'", serial)); + } + + return new Component(serial, ModelHelper.toJavaString(parts[3]), "1".equals(parts[4]), + Integer.parseInt(parts[5]), Integer.parseInt(parts[7])); + } + + public String generateCommandString(final String command) { + StringJoiner joiner = new StringJoiner(" "); + joiner.add(command).add(ModelHelper.toHubString(serialNumber.toString())); + + // Status not yet implemented in hub + joiner.add("0"); + + joiner.add(ModelHelper.toHubString(name)).add(reverse ? "1" : "0").add(Integer.toString(zoneId)).add("-1"); + + // Active Override ID not implemented in hub for components yet + joiner.add(Integer.toString(temperatureSensorForZoneId)); + return joiner.toString(); + } + + public SerialNumber getSerialNumber() { + return serialNumber; + } + + public String getName() { + return name; + } + + public boolean inReverse() { + return reverse; + } + + public int getZoneId() { + return zoneId; + } + + public int getTemperatureSensorForZoneId() { + return temperatureSensorForZoneId; + } + + public double getTemperature() { + return temperature; + } + + public void setTemperature(double temperature) { + this.temperature = temperature; + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/ComponentRegister.java b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/ComponentRegister.java new file mode 100644 index 00000000000..42b752e1abc --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/ComponentRegister.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal.model; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import javax.validation.constraints.NotNull; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Stores a mapping between component ids and components that exists. + * + * @author Jørgen Austvik - Initial contribution + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +public final class ComponentRegister { + + private final @NotNull Map register = new HashMap(); + + /** + * Stores a new Component in the register. If a component exists with the same id, that value is overwritten. + * + * @param component The Component to store. + */ + public void put(Component component) { + register.put(component.getSerialNumber(), component); + } + + /** + * Removes a component from the registry. + * + * @param componentId The component to remove + * @return The component that is removed. Null if the component is not found. + */ + public @Nullable Component remove(SerialNumber componentId) { + return register.remove(componentId); + } + + /** + * Returns a component from the registry. + * + * @param componentId The id of the component to return. + * @return Returns the component, or null if it doesn't exist in the regestry. + */ + public @Nullable Component get(SerialNumber componentId) { + return register.get(componentId); + } + + public Collection values() { + return register.values(); + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/Hub.java b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/Hub.java new file mode 100644 index 00000000000..659982e90ee --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/Hub.java @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal.model; + +import java.time.Duration; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Contains information about the Hub we are communicating with. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class Hub { + + private final SerialNumber serialNumber; + + private final String name; + + private int activeOverrideId; + + private final int defaultAwayOverrideLength; + + private final String softwareVersion; + + private final String hardwareVersion; + + private final String productionDate; + + public Hub(SerialNumber serialNumber, String name, int defaultAwayOverrideLength, int activeOverrideId, + String softwareVersion, String hardwareVersion, String productionDate) { + this.serialNumber = serialNumber; + this.name = name; + this.defaultAwayOverrideLength = defaultAwayOverrideLength; + this.activeOverrideId = activeOverrideId; + this.softwareVersion = softwareVersion; + this.hardwareVersion = hardwareVersion; + this.productionDate = productionDate; + } + + public static Hub fromH05(String h05) throws NoboDataException { + String parts[] = h05.split(" ", 8); + + if (parts.length != 8) { + throw new NoboDataException( + String.format("Unexpected number of parts from hub on H5 call: %d", parts.length)); + } + + return new Hub(new SerialNumber(ModelHelper.toJavaString(parts[1])), ModelHelper.toJavaString(parts[2]), + Integer.parseInt(parts[3]), Integer.parseInt(parts[4]), ModelHelper.toJavaString(parts[5]), + ModelHelper.toJavaString(parts[6]), ModelHelper.toJavaString(parts[7])); + } + + public String generateCommandString(final String command) { + return String.join(" ", command, serialNumber.toString(), ModelHelper.toHubString(name), + Integer.toString(defaultAwayOverrideLength), Integer.toString(activeOverrideId), + ModelHelper.toHubString(softwareVersion), ModelHelper.toHubString(hardwareVersion), + ModelHelper.toHubString(productionDate)); + } + + public SerialNumber getSerialNumber() { + return serialNumber; + } + + public String getName() { + return name; + } + + public Duration getDefaultAwayOverrideLength() { + return Duration.ofMinutes(defaultAwayOverrideLength); + } + + public int getActiveOverrideId() { + return activeOverrideId; + } + + public void setActiveOverrideId(int id) { + activeOverrideId = id; + } + + public String getSoftwareVersion() { + return softwareVersion; + } + + public String getHardwareVersion() { + return hardwareVersion; + } + + public String getProductionDate() { + return productionDate; + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/ModelHelper.java b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/ModelHelper.java new file mode 100644 index 00000000000..3d1f10fa462 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/ModelHelper.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal.model; + +import java.time.DateTimeException; +import java.time.LocalDateTime; +import java.time.format.DateTimeParseException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.nobohub.internal.NoboHubBindingConstants; + +/** + * Helper class for converting data to/from Nobø Hub. + * + * @author Jørgen Austvik - Initial contribution + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +public final class ModelHelper { + + /** + * Converts a String returned form Nobø hub to a normal Java string. + * + * @param noboString String where Char 160 (nobr space is used for space) + * @return String with normal spaces. + */ + static String toJavaString(final String noboString) { + return noboString.replace((char) 160, ' '); + } + + /** + * Converts a String in java to a string the Nobø hub can understand (fix spaces). + * + * @param javaString String to send to Nobø hub + * @return String with Nobø hub spaces + */ + static String toHubString(final String javaString) { + return javaString.replace(' ', (char) 160); + } + + /** + * Creates a Java date string from a date string returned from the Nobø Hub. + * + * @param noboDateString Date string from Nobø, like '202001221832' or '-1' + * @return Java date for the returned string (or null if -1 is returned) + */ + @Nullable + static LocalDateTime toJavaDate(final String noboDateString) throws NoboDataException { + if ("-1".equals(noboDateString)) { + return null; + } + + try { + return LocalDateTime.parse(noboDateString, NoboHubBindingConstants.DATE_FORMAT_MINUTES); + } catch (DateTimeParseException pe) { + throw new NoboDataException(String.format("Failed parsing string %s", noboDateString), pe); + } + } + + static String toHubDateMinutes(final @Nullable LocalDateTime date) { + if (null == date) { + return "-1"; + } + + try { + return date.format(NoboHubBindingConstants.DATE_FORMAT_MINUTES); + } catch (DateTimeException dte) { + return "-1"; + } + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/NoboCommunicationException.java b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/NoboCommunicationException.java new file mode 100644 index 00000000000..a437652b660 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/NoboCommunicationException.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal.model; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Exception thrown when failing to communicate with the hub. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class NoboCommunicationException extends Exception { + + private static final long serialVersionUID = -620277949858983367L; + + public NoboCommunicationException(String message) { + super(message); + } + + public NoboCommunicationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/NoboDataException.java b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/NoboDataException.java new file mode 100644 index 00000000000..cbdecb8e682 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/NoboDataException.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal.model; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Exception thrown when the data received from the hub has unexpected format. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class NoboDataException extends Exception { + + private static final long serialVersionUID = -620277949858983367L; + + public NoboDataException(String message) { + super(message); + } + + public NoboDataException(String message, Throwable parent) { + super(message, parent); + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/OverrideMode.java b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/OverrideMode.java new file mode 100644 index 00000000000..0655131b181 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/OverrideMode.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal.model; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The mode of the {@link OverridePlan}. What the value is overridden to. + * + * @author Jørgen Austvik - Initial contribution + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +public enum OverrideMode { + + NORMAL(0), + COMFORT(1), + ECO(2), + AWAY(3); + + private final int numValue; + + OverrideMode(int numValue) { + this.numValue = numValue; + } + + public static OverrideMode getByNumber(int value) throws NoboDataException { + switch (value) { + case 0: + return NORMAL; + case 1: + return COMFORT; + case 2: + return ECO; + case 3: + return AWAY; + default: + throw new NoboDataException(String.format("Unknown override mode %d", value)); + } + } + + public int getNumValue() { + return numValue; + } + + public static OverrideMode getByName(String name) throws NoboDataException { + if (name.isEmpty()) { + throw new NoboDataException("Missing name"); + } + + if ("Normal".equalsIgnoreCase(name)) { + return NORMAL; + } else if ("Comfort".equalsIgnoreCase(name)) { + return COMFORT; + } else if ("Eco".equalsIgnoreCase(name)) { + return ECO; + } else if ("Away".equalsIgnoreCase(name)) { + return AWAY; + } + + throw new NoboDataException(String.format("Unknown name of override mode: '%s'", name)); + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/OverridePlan.java b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/OverridePlan.java new file mode 100644 index 00000000000..d56511aa822 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/OverridePlan.java @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal.model; + +import java.time.LocalDateTime; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * An override is when the normal weekly program is not followed because it is specified by pressing a switch or using + * an app. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public final class OverridePlan { + + private final int id; + private final OverrideMode mode; + private final OverrideType type; + private final @Nullable LocalDateTime startTime; + private final @Nullable LocalDateTime endTime; + private final OverrideTarget target; + private final int targetId; + + public OverridePlan(int id, OverrideMode mode, OverrideType type, @Nullable LocalDateTime startTime, + @Nullable LocalDateTime endTime, OverrideTarget target, int targetId) { + this.id = id; + this.mode = mode; + this.type = type; + this.startTime = startTime; + this.endTime = endTime; + this.target = target; + this.targetId = targetId; + } + + public static OverridePlan fromH04(String h04) throws NoboDataException { + String[] parts = h04.split(" ", 8); + + if (parts.length != 8) { + throw new NoboDataException( + String.format("Unexpected number of parts from hub on H4 call: %d", parts.length)); + } + + return new OverridePlan(Integer.parseInt(parts[1]), OverrideMode.getByNumber(Integer.parseInt(parts[2])), + OverrideType.getByNumber(Integer.parseInt(parts[3])), ModelHelper.toJavaDate(parts[4]), + ModelHelper.toJavaDate(parts[5]), OverrideTarget.getByNumber(Integer.parseInt(parts[6])), + Integer.parseInt(parts[7])); + } + + public static OverridePlan fromMode(OverrideMode mode, LocalDateTime date) { + return new OverridePlan(1, mode, OverrideType.NOW, null, null, OverrideTarget.HUB, -1); + } + + public String generateCommandString(final String command) { + return String.join(" ", command, Integer.toString(id), Integer.toString(mode.getNumValue()), + Integer.toString(type.getNumValue()), ModelHelper.toHubDateMinutes(startTime), + ModelHelper.toHubDateMinutes(endTime), Integer.toString(target.getNumValue()), + Integer.toString(targetId)); + } + + public int getId() { + return id; + } + + public OverrideMode getMode() { + return mode; + } + + public OverrideType getType() { + return type; + } + + public @Nullable LocalDateTime startTime() { + return startTime; + } + + public @Nullable LocalDateTime endTime() { + return endTime; + } + + public OverrideTarget getTarget() { + return target; + } + + public int getTargetId() { + return targetId; + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/OverrideRegister.java b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/OverrideRegister.java new file mode 100644 index 00000000000..5a24ce4a735 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/OverrideRegister.java @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal.model; + +import java.util.HashMap; +import java.util.Map; + +import javax.validation.constraints.NotNull; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Stores a mapping between override ids and overrides that are in place. + * + * @author Jørgen Austvik - Initial contribution + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +public final class OverrideRegister { + + private final @NotNull Map register = new HashMap<>(); + + /** + * Stores a new Override in the register. If an override exists with the same id, that value is overwritten. + * + * @param overridePlan The Override to store. + */ + public void put(OverridePlan overridePlan) { + register.put(overridePlan.getId(), overridePlan); + } + + /** + * Removes an override from the registry. + * + * @param overrideId The override to remove + * @return The override that is removed. Null if the override is not found. + */ + public @Nullable OverridePlan remove(int overrideId) { + return register.remove(overrideId); + } + + /** + * Returns an Override from the registry. + * + * @param overrideId The id of the override to return. + * @return Returns the override, or null if it doesnt exist in the regestry. + */ + public @Nullable OverridePlan get(int overrideId) { + return register.get(overrideId); + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/OverrideTarget.java b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/OverrideTarget.java new file mode 100644 index 00000000000..78137b4225a --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/OverrideTarget.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal.model; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The target of the {@link OverridePlan}. What it applies to. + * + * @author Jørgen Austvik - Initial contribution + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +public enum OverrideTarget { + + HUB(0), + ZONE(1), + COMPONENT(2); + + private final int numValue; + + private OverrideTarget(int numValue) { + this.numValue = numValue; + } + + public static OverrideTarget getByNumber(int value) throws NoboDataException { + switch (value) { + case 0: + return HUB; + case 1: + return ZONE; + case 2: + return COMPONENT; + default: + throw new NoboDataException(String.format("Unknown override target %d", value)); + } + } + + public int getNumValue() { + return numValue; + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/OverrideType.java b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/OverrideType.java new file mode 100644 index 00000000000..e8e81c0c637 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/OverrideType.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal.model; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The type of the {@link OverridePlan}. How long does it last. + * + * @author Jørgen Austvik - Initial contribution + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +public enum OverrideType { + + NOW(0), + TIMER(1), + FROM_TO(2), + CONSTANT(3); + + private final int numValue; + + OverrideType(int numValue) { + this.numValue = numValue; + } + + public static OverrideType getByNumber(int value) throws NoboDataException { + switch (value) { + case 0: + return NOW; + case 1: + return TIMER; + case 2: + return FROM_TO; + case 3: + return CONSTANT; + default: + throw new NoboDataException(String.format("Unknown override type %d", value)); + } + } + + public int getNumValue() { + return numValue; + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/SerialNumber.java b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/SerialNumber.java new file mode 100644 index 00000000000..65bee469a30 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/SerialNumber.java @@ -0,0 +1,116 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal.model; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.nobohub.internal.NoboHubBindingConstants; + +/** + * Nobø serial numbers are 12 digits where 3 and 3 digits form 2 bytes as decimal. In total 32 bits. + * + * @author Jørgen Austvik - Initial contribution + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +public final class SerialNumber { + + private final String serialNumber; + + public SerialNumber(String serialNumber) { + this.serialNumber = serialNumber.trim(); + } + + public boolean isWellFormed() { + if (serialNumber.length() != 12) { + return false; + } + + List parts = new ArrayList<>(4); + for (int i = 0; i < 4; i++) { + parts.add(serialNumber.substring((i * 3), (i * 3) + 3)); + } + + if (parts.size() != 4) { + return false; + } + + for (String part : parts) { + try { + int num = Integer.parseInt(part); + if (num < 0 || num > 255) { + return false; + } + } catch (NumberFormatException nfe) { + return false; + } + } + + return true; + } + + /** + * Returns the type string. + */ + public String getTypeIdentifier() { + if (!isWellFormed()) { + return "Unknown"; + } + + return serialNumber.substring(0, 3); + } + + /** + * Returns the type of this component. + */ + public String getComponentType() { + String id = getTypeIdentifier(); + String type = getTypeForSerialNumber(id); + if (null != type) { + return type; + } + + return "Unknown, please contact maintainer to add a new type for " + serialNumber; + } + + private @Nullable String getTypeForSerialNumber(String id) { + return NoboHubBindingConstants.SERIALNUMBERS_FOR_TYPES.get(id); + } + + @Override + public String toString() { + return serialNumber; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || obj.getClass() != this.getClass()) { + return false; + } + + SerialNumber other = (SerialNumber) obj; + return this.serialNumber.equals(other.serialNumber); + } + + @Override + public int hashCode() { + return this.serialNumber.hashCode(); + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/Temperature.java b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/Temperature.java new file mode 100644 index 00000000000..a617a3a7ba7 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/Temperature.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal.model; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Nobø serial numbers are 12 digits where 3 and 3 digits form 2 bytes as decimal. In total 32 bits. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public final class Temperature { + + private final SerialNumber serialNumber; + private final double temperature; + + public Temperature(SerialNumber serialNumber, double temperature) { + this.serialNumber = serialNumber; + this.temperature = temperature; + } + + public static Temperature fromY02(String y02) throws NoboDataException { + String parts[] = y02.split(" ", 3); + if (parts.length != 3) { + throw new NoboDataException( + String.format("Unexpected number of parts from hub on Y02 call: %d", parts.length)); + } + + if (parts[2] == null) { + throw new NoboDataException("Missing temperature data"); + } + + SerialNumber serialNumber = new SerialNumber(parts[1]); + double temp = Double.NaN; + + if (!"N/A".equals(parts[2])) { + try { + temp = Double.parseDouble(parts[2]); + } catch (NumberFormatException nfe) { + throw new NoboDataException( + String.format("Failed to parse temperature %s: %s", parts[2], nfe.getMessage()), nfe); + } + } + + return new Temperature(serialNumber, temp); + } + + public SerialNumber getSerialNumber() { + return serialNumber; + } + + public double getTemperature() { + return temperature; + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/WeekProfile.java b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/WeekProfile.java new file mode 100644 index 00000000000..abef0f3c890 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/WeekProfile.java @@ -0,0 +1,117 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal.model; + +import java.time.DayOfWeek; +import java.time.LocalDateTime; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.nobohub.internal.NoboHubBindingConstants; + +/** + * The normal week profile (used when no {@link OverridePlan}s exist). + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public final class WeekProfile { + + private final int id; + private final String name; + private final String profile; + + public WeekProfile(int id, String name, String profile) { + this.id = id; + this.name = name; + this.profile = profile; + } + + public static WeekProfile fromH03(String h03) throws NoboDataException { + String[] parts = h03.split(" ", 4); + + if (parts.length != 4) { + throw new NoboDataException( + String.format("Unexpected number of parts from hub on H3 call: %d", parts.length)); + } + + return new WeekProfile(Integer.parseInt(parts[1]), ModelHelper.toJavaString(parts[2]), + ModelHelper.toJavaString(parts[3])); + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } + + public String getProfile() { + return profile; + } + + /** + * Returns the current status on the week profile (unless there is an override). + * + * @param time The current time + * @return The current status (according to the week profile) + */ + public WeekProfileStatus getStatusAt(LocalDateTime time) throws NoboDataException { + final DayOfWeek weekDay = time.getDayOfWeek(); + final int dayNumber = weekDay.getValue(); + final String timeString = time.format(NoboHubBindingConstants.TIME_FORMAT_MINUTES); + String[] parts = profile.split(","); + + int dayCounter = 0; + for (int i = 0; i < parts.length; i++) { + String current = parts[i]; + if (current.startsWith("0000")) { + dayCounter++; + } + + if (current.length() != 5) { + throw new NoboDataException("Illegal week profile entry: " + current); + } + + if (dayNumber == dayCounter) { + String next = "24000"; + if (i + 1 < parts.length) { + if (!parts[i + 1].startsWith("0000")) { + next = parts[i + 1]; + } + } + + if (next.length() != 5) { + throw new NoboDataException("Illegal week profile entry for next entry: " + next); + } + + try { + String currentTime = current.substring(0, 4); + String nextTime = next.substring(0, 4); + if (currentTime.compareTo(timeString) <= 0 && timeString.compareTo(nextTime) < 0) { + try { + return WeekProfileStatus.getByNumber(Integer.parseInt(String.valueOf(current.charAt(4)))); + } catch (NumberFormatException nfe) { + throw new NoboDataException("Failed parsing week profile entry: " + current, nfe); + } + } + } catch (IndexOutOfBoundsException oobe) { + throw new NoboDataException("Illegal time string" + current + ", " + next, oobe); + } + } + } + + throw new NoboDataException( + String.format("Failed to calculate %s for day %d in '%s'", timeString, dayNumber, profile)); + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/WeekProfileRegister.java b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/WeekProfileRegister.java new file mode 100644 index 00000000000..b8cc4df700a --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/WeekProfileRegister.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal.model; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import javax.validation.constraints.NotNull; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Stores a mapping between week profile ids and week profiles that exists. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public final class WeekProfileRegister { + + private @NotNull Map register = new HashMap(); + + /** + * Stores a new week profile in the register. If an week profile exists with the same id, that value is overwritten. + * + * @param profile The week profile to store. + */ + public void put(WeekProfile profile) { + register.put(profile.getId(), profile); + } + + /** + * Removes a WeekProfile from the registry. + * + * @param weekProfileId The week profile to remove + * @return The week profile that is removed. Null if the week profile is not found. + */ + public @Nullable WeekProfile remove(int weekProfileId) { + return register.remove(weekProfileId); + } + + /** + * Returns a WeekProfile from the registry. + * + * @param weekProfileId The id of the week profile to return. + * @return Returns the week profile, or null if it doesnt exist in the registry. + */ + public @Nullable WeekProfile get(int weekProfileId) { + return register.get(weekProfileId); + } + + /** + * Returns all WeekProfiles from the registry. + * + * @return Returns the week profile, or empty list if no profiles. + */ + public Collection values() { + return register.values(); + } + + public boolean isEmpty() { + return register.isEmpty(); + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/WeekProfileStatus.java b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/WeekProfileStatus.java new file mode 100644 index 00000000000..51b09d2cee7 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/WeekProfileStatus.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal.model; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The status of the {@link WeekProfile}. What the value is in the week profile. Status OFF is matched both to value 3 + * and 4, while the documentation says 3, Hub with Hardware version 11123610_rev._1 and production date 20180305 + * will send value 4 for OFF. + * compatibility. + * + * @author Jørgen Austvik - Initial contribution + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +public enum WeekProfileStatus { + + ECO(0), + COMFORT(1), + AWAY(2), + OFF(3); + + private final int numValue; + + private WeekProfileStatus(int numValue) { + this.numValue = numValue; + } + + public static WeekProfileStatus getByNumber(int value) throws NoboDataException { + switch (value) { + case 0: + return ECO; + case 1: + return COMFORT; + case 2: + return AWAY; + case 3: + case 4: + return OFF; + default: + throw new NoboDataException(String.format("Unknown week profile status %d", value)); + } + } + + public int getNumValue() { + return numValue; + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/Zone.java b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/Zone.java new file mode 100644 index 00000000000..312f03ea41b --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/Zone.java @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal.model; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * A Zone contains one or more {@link Component}s. + * + * @author Jørgen Austvik - Initial contribution + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +public final class Zone { + + private final int id; + private final String name; + private int activeWeekProfileId; + private int comfortTemperature; + private int ecoTemperature; + private final boolean allowOverrides; + private @Nullable Double temperature; + + public Zone(int id, String name, int activeWeekProfileId, int comfortTemperature, int ecoTemperature, + boolean allowOverrides) throws NoboDataException { + this.id = id; + this.name = name; + this.activeWeekProfileId = activeWeekProfileId; + this.comfortTemperature = comfortTemperature; + this.ecoTemperature = ecoTemperature; + this.allowOverrides = allowOverrides; + } + + public static Zone fromH01(String h01) throws NoboDataException { + String parts[] = h01.split(" ", 8); + + if (parts.length != 8) { + throw new NoboDataException( + String.format("Unexpected number of parts from hub on H1 call: %d", parts.length)); + } + + return new Zone(Integer.parseInt(parts[1]), ModelHelper.toJavaString(parts[2]), Integer.parseInt(parts[3]), + Integer.parseInt(parts[4]), Integer.parseInt(parts[5]), "1".equals(parts[6])); + } + + public String generateCommandString(final String command) { + return String.join(" ", command, Integer.toString(id), ModelHelper.toHubString(name), + Integer.toString(activeWeekProfileId), Integer.toString(comfortTemperature), + Integer.toString(ecoTemperature), allowOverrides ? "1" : "0", "-1"); // "Active override id" is + // deprecated + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } + + public int getActiveWeekProfileId() { + return activeWeekProfileId; + } + + public int getComfortTemperature() { + return comfortTemperature; + } + + public int getEcoTemperature() { + return ecoTemperature; + } + + public boolean getAllowOverrides() { + return allowOverrides; + } + + public void setTemperature(@Nullable Double temperature) { + this.temperature = temperature; + } + + public @Nullable Double getTemperature() { + return temperature; + } + + public void setComfortTemperature(int temp) { + comfortTemperature = temp; + } + + public void setEcoTemperature(int temp) { + ecoTemperature = temp; + } + + public void setWeekProfile(int weekProfileId) { + activeWeekProfileId = weekProfileId; + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/ZoneRegister.java b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/ZoneRegister.java new file mode 100644 index 00000000000..d3daa00fa4f --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/java/org/openhab/binding/nobohub/internal/model/ZoneRegister.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal.model; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import javax.validation.constraints.NotNull; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Stores a mapping between zone ids and zones that exists. + * + * @author Jørgen Austvik - Initial contribution + * @author Espen Fossen - Initial contribution + */ +@NonNullByDefault +public final class ZoneRegister { + + private final @NotNull Map register = new HashMap(); + + /** + * Stores a new Zone in the register. If a zone exists with the same id, that value is overwritten. + * + * @param zone The Zone to store. + */ + public void put(Zone zone) { + register.put(zone.getId(), zone); + } + + /** + * Removes a zone from the registry. + * + * @param zoneId The zone to remove + * @return The zone that is removed. Null if the zone is not found. + */ + public @Nullable Zone remove(int zoneId) { + return register.remove(zoneId); + } + + /** + * Returns a Zone from the registry. + * + * @param zoneId The id of the zone to return. + * @return Returns the zone, or null if it doesnt exist in the regestry. + */ + public @Nullable Zone get(int zoneId) { + return register.get(zoneId); + } + + public Collection values() { + return register.values(); + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.nobohub/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 00000000000..39d0ea55f15 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,9 @@ + + + + Glen Dimplex Nobø Hub Binding + This is the binding for Glen Dimplex Nobø Hub. + + diff --git a/bundles/org.openhab.binding.nobohub/src/main/resources/OH-INF/i18n/nobohub.properties b/bundles/org.openhab.binding.nobohub/src/main/resources/OH-INF/i18n/nobohub.properties new file mode 100644 index 00000000000..0d0174125b5 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/resources/OH-INF/i18n/nobohub.properties @@ -0,0 +1,57 @@ +# binding + +binding.nobohub.name = Glen Dimplex Nobø Hub Binding +binding.nobohub.description = This is the binding for Glen Dimplex Nobø Hub. + +# thing types + +thing-type.nobohub.component.label = Component +thing-type.nobohub.component.description = A component is an oven, a switch or a floor thermostat +thing-type.nobohub.nobohub.label = Nobø Hub +thing-type.nobohub.nobohub.description = Nobø Hub Bridge Binding +thing-type.nobohub.zone.label = Zone +thing-type.nobohub.zone.description = A zone can contain several Nobø devices + +# thing types config + +thing-type.config.nobohub.component.serialNumber.label = Serial Number +thing-type.config.nobohub.component.serialNumber.description = Serial number of the component (12 digits) +thing-type.config.nobohub.nobohub.hostName.label = Host Name +thing-type.config.nobohub.nobohub.hostName.description = Host Name/IP address of the Nobø Hub +thing-type.config.nobohub.nobohub.keepaliveInterval.label = Polling interval +thing-type.config.nobohub.nobohub.keepaliveInterval.description = Polling interval (seconds). Default: 14. +thing-type.config.nobohub.nobohub.serialNumber.label = Serial Number +thing-type.config.nobohub.nobohub.serialNumber.description = Serial number of the Nobø hub (12 numbers, no spaces) +thing-type.config.nobohub.zone.id.label = Id +thing-type.config.nobohub.zone.id.description = Id of the Zone + +# channel types + +channel-type.nobohub.activeOverrideName-channel-type.label = Active Override +channel-type.nobohub.activeOverrideName-channel-type.description = Mode of active override, using one of the predefined states supported +channel-type.nobohub.activeOverrideName-channel-type.state.option.NORMAL = Normal +channel-type.nobohub.activeOverrideName-channel-type.state.option.COMFORT = Comfort +channel-type.nobohub.activeOverrideName-channel-type.state.option.ECO = Eco +channel-type.nobohub.activeOverrideName-channel-type.state.option.Away = Away +channel-type.nobohub.activeWeekProfile-channel-type.label = Active Week Profile Id +channel-type.nobohub.activeWeekProfile-channel-type.description = Id of the active week profile, set via the Nobø app +channel-type.nobohub.activeWeekProfileName-channel-type.label = Active Week Profile Name +channel-type.nobohub.activeWeekProfileName-channel-type.description = Name of the active week profile, set via the Nobø app +channel-type.nobohub.comfort-temperature-channel-type.label = Comfort Temperature +channel-type.nobohub.comfort-temperature-channel-type.description = The preferred Comfort temperature level set on the heater or in the binding +channel-type.nobohub.eco-temperature-channel-type.label = Eco Temperature +channel-type.nobohub.eco-temperature-channel-type.description = The preferred Eco temperature level set on the heater or in the binding +channel-type.nobohub.temperature-channel-type.label = Current Temperature +channel-type.nobohub.temperature-channel-type.description = The current temperature from a device that supports reporting temperatures +channel-type.nobohub.weekProfiles-channel-type.label = Week Profiles +channel-type.nobohub.weekProfiles-channel-type.description = Name of the active week profile, set via the Nobø app + +# User Messages +message.missing.serial = Missing serial number in configuration +message.bridge.status.failed = Failed to get status: {0} +message.bridge.missing.hostname = Missing host name in configuration +message.bridge.connection.failed = Failed to connect, check network connectivity and configuration +message.component.illegal.serial = Illegal serial number: {0} +message.component.notfound = Could not find Component with serial number {0} for channel {1} +message.component.missing.id = Id not set for channel {0} +message.zone.notfound = Could not find Zone with id {0} for channel {1} diff --git a/bundles/org.openhab.binding.nobohub/src/main/resources/OH-INF/i18n/nobohub_no.properties b/bundles/org.openhab.binding.nobohub/src/main/resources/OH-INF/i18n/nobohub_no.properties new file mode 100644 index 00000000000..0a5546a666f --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/resources/OH-INF/i18n/nobohub_no.properties @@ -0,0 +1,57 @@ +# binding + +binding.nobohub.name = Glen Dimplex Nobø Hub Binding +binding.nobohub.description = Dette er en binding for Glen Dimplex Nobø Hub. + +# thing types + +thing-type.nobohub.component.label = Komponent +thing-type.nobohub.component.description = En komponent kan være en panelovn, bryter eller gulv termostat +thing-type.nobohub.nobohub.label = Nobø Hub +thing-type.nobohub.nobohub.description = Nobø Hub Bru Binding +thing-type.nobohub.zone.label = Sone +thing-type.nobohub.zone.description = En sone kan inneholde flere Nobø enheter + +# thing types config + +thing-type.config.nobohub.nobohub.serialNumber.label = Serialnummer +thing-type.config.nobohub.nobohub.serialNumber.description = Nobø Hub serialnummer (12 tall) +thing-type.config.nobohub.nobohub.hostName.label = Tjeneradresse +thing-type.config.nobohub.nobohub.hostName.description = Tjener eller IP addresse til Nobø Hub +thing-type.config.nobohub.nobohub.keepaliveInterval.label = Tidsintervall +thing-type.config.nobohub.nobohub.keepaliveInterval.description = Tidsintervall (sekunder). Standardinnstilling: 14. +thing-type.config.nobohub.component.serialNumber.label = Serialnummer +thing-type.config.nobohub.component.serialNumber.description = Serialnummer for komponent (12 tall, uten mellomrom) +thing-type.config.nobohub.zone.id.label = Id +thing-type.config.nobohub.zone.id.description = Id for sone + +# channel types + +channel-type.nobohub.activeOverrideName-channel-type.label = Aktiv Overstyring +channel-type.nobohub.activeOverrideName-channel-type.description = Modus for aktiv overstyring, bruker en av de predefinerte typene som støttes +channel-type.nobohub.activeOverrideName-channel-type.state.option.NORMAL = Normal +channel-type.nobohub.activeOverrideName-channel-type.state.option.COMFORT = Komfort +channel-type.nobohub.activeOverrideName-channel-type.state.option.ECO = Eco +channel-type.nobohub.activeOverrideName-channel-type.state.option.Away = Borte +channel-type.nobohub.activeWeekProfile-channel-type.label = Aktiv Ukeprofil Id +channel-type.nobohub.activeWeekProfile-channel-type.description = Id på nåværende aktiv ukesprofil +channel-type.nobohub.activeWeekProfileName-channel-type.label = Aktiv Ukeprofil Navn +channel-type.nobohub.activeWeekProfileName-channel-type.description = Navn på nåværende aktiv ukesprofil +channel-type.nobohub.comfort-temperature-channel-type.label = Komfort Temperatur +channel-type.nobohub.comfort-temperature-channel-type.description = Ønsket Komfort temperaturnivå satt på panel eller i binding +channel-type.nobohub.eco-temperature-channel-type.label = Eco Temperatur +channel-type.nobohub.eco-temperature-channel-type.description = Ønsket Eco temperaturnivå satt på panel eller i binding +channel-type.nobohub.temperature-channel-type.label = Nåværende Temperatur +channel-type.nobohub.temperature-channel-type.description = Nåværende temperatur fra en enhet som støtter rapportering av temperaturer +channel-type.nobohub.weekProfiles-channel-type.label = Ukeprofiler +channel-type.nobohub.weekProfiles-channel-type.description = Tilgjengelige ukesprofiler, satt opp via Nobø app + +# User Messages +message.missing.serial = Mangler serialnummer i konfigurasjon +message.bridge.status.failed = Kunne ikke hente status: {0} +message.bridge.missing.hostname = Mangler tjenernavn i konfigurasjon +message.bridge.connection.failed = Kunne ikke koble til, sjekk nettverksforbindelsen og konfigurasjon +message.component.illegal.serial = Serialnummer er ukjent eller feil: {0} +message.component.notfound = Kunne ikke finne Komponent med serialnummer {0} for kanal {1} +message.component.missing.id = Id er ikke satt for kanal {0} +message.zone.notfound = Kunne ikke finne Sone med id {0} for kanal {1} diff --git a/bundles/org.openhab.binding.nobohub/src/main/resources/OH-INF/thing/bridge.xml b/bundles/org.openhab.binding.nobohub/src/main/resources/OH-INF/thing/bridge.xml new file mode 100644 index 00000000000..1d6e4fdcab5 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/resources/OH-INF/thing/bridge.xml @@ -0,0 +1,38 @@ + + + + + + Nobo Hub Bridge Binding + + + + + + + + Glen Dimplex Nobo + + serialNumber + + + + + Serial number of the Nobo hub (12 numbers, no spaces) + + + + Host Name/IP address of the Nobo Hub + + + + Polling interval (seconds). Default: 14 + 14 + + + + + diff --git a/bundles/org.openhab.binding.nobohub/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.nobohub/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 00000000000..56708e048a5 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,137 @@ + + + + + + + + + + A zone can contain several Nobo devices + + + + + + + + + + + + Glen Dimplex Nobo + + name + + + + + Id of the Zone + + + + + + + + + + + A component is an oven, a switch or a floor thermostat + + + + + + + Glen Dimplex Nobo + + serialNumber + + + + + Serial number of the component (12 digits) + + + + + + String + + Name of active override, using one of the predefined states supported + Heating + + + + + + + + + + + + Number:Temperature + + The preferred Eco temperature level set on the heater or in the binding + Temperature + + Setpoint + Temperature + + + + + + Number:Temperature + + The preferred Comfort temperature level set on the heater or in the binding + Temperature + + Setpoint + Temperature + + + + + + Number:Temperature + + The current temperature from a device that supports reporting temperatures + Temperature + + Measurement + Temperature + + + + + + String + + Name of the active week profile, set via the Nobo app + Heating + + + + + Number + + Id of the active week profile, set via the Nobo app + Heating + + + + + String + + List of active week profiles, set via the Nobo app + Heating + + + + diff --git a/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/ComponentRegisterTest.java b/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/ComponentRegisterTest.java new file mode 100644 index 00000000000..649f630fbb1 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/ComponentRegisterTest.java @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal.model; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for ComponentRegister model object. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class ComponentRegisterTest { + + @Test + public void testPutGet() throws NoboDataException { + Component c = Component.fromH02("H02 186170024143 0 Kontor 0 1 -1 -1"); + ComponentRegister sut = new ComponentRegister(); + sut.put(c); + Assertions.assertEquals(c, sut.get(c.getSerialNumber())); + } + + @Test + public void testPutOverwrite() throws NoboDataException { + Component c1 = Component.fromH02("H02 186170024143 0 Kontor 0 1 -1 -1"); + Component c2 = Component.fromH02("H02 186170024143 0 Bad 0 1 -1 -1"); + ComponentRegister sut = new ComponentRegister(); + sut.put(c1); + sut.put(c2); + Assertions.assertEquals(c2, sut.get(c2.getSerialNumber())); + } + + @Test + public void testRemove() throws NoboDataException { + Component c = Component.fromH02("H02 186170024143 0 Kontor 0 1 -1 -1"); + ComponentRegister sut = new ComponentRegister(); + sut.put(c); + Component res = sut.remove(c.getSerialNumber()); + Assertions.assertEquals(c, res); + } + + @Test + public void testRemoveUnknown() { + ComponentRegister sut = new ComponentRegister(); + Component res = sut.remove(new SerialNumber("123123123123")); + Assertions.assertEquals(null, res); + } + + @Test + public void testGetUnknown() { + ComponentRegister sut = new ComponentRegister(); + Component z = sut.get(new SerialNumber("123123123123")); + Assertions.assertEquals(null, z); + } + + @Test + public void testValues() throws NoboDataException { + Component c1 = Component.fromH02("H02 186170024141 0 Kontor 0 1 -1 -1"); + Component c2 = Component.fromH02("H02 186170024142 0 Soverom 0 1 -1 -1"); + ComponentRegister sut = new ComponentRegister(); + sut.put(c1); + sut.put(c2); + Assertions.assertEquals(2, sut.values().size()); + Assertions.assertEquals(true, sut.values().contains(c1)); + Assertions.assertEquals(true, sut.values().contains(c2)); + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/ComponentTest.java b/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/ComponentTest.java new file mode 100644 index 00000000000..3d1b97b3277 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/ComponentTest.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for Component model object. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class ComponentTest { + @Test + public void testParseH02() throws NoboDataException { + Component comp = Component.fromH02("H02 186170024143 0 Kontor 0 1 -1 -1"); + comp.setTemperature(12.3); + assertEquals(new SerialNumber("186170024143"), comp.getSerialNumber()); + assertEquals("Kontor", comp.getName()); + assertEquals(1, comp.getZoneId()); + assertEquals(-1, comp.getTemperatureSensorForZoneId()); + assertFalse(comp.inReverse()); + assertEquals(12.3, comp.getTemperature(), 0.1); + } + + @Test + public void testGenerateU03() throws NoboDataException { + Component comp = Component.fromH02("H02 186170024143 0 Kontor 0 1 -1 -1"); + assertEquals("U02 186170024143 0 Kontor 0 1 -1 -1", comp.generateCommandString("U02")); + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/HubTest.java b/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/HubTest.java new file mode 100644 index 00000000000..e48592aed0e --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/HubTest.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.Duration; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for Hub model object. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class HubTest { + + @Test + public void testParseH05() throws NoboDataException { + Hub hub = Hub.fromH05("H05 102000092118 My Eco Hub 2880 4 114 11123610_rev._1 20190426"); + assertEquals(new SerialNumber("102000092118"), hub.getSerialNumber()); + assertEquals("My Eco Hub", hub.getName()); + assertEquals(Duration.ofDays(2), hub.getDefaultAwayOverrideLength()); + assertEquals(4, hub.getActiveOverrideId()); + assertEquals("114", hub.getSoftwareVersion()); + assertEquals("11123610_rev._1", hub.getHardwareVersion()); + assertEquals("20190426", hub.getProductionDate()); + } + + @Test + public void testParseV03() throws NoboDataException { + Hub hub = Hub.fromH05("V03 102000092118 My Eco Hub 2880 14 114 11123610_rev._1 20190426"); + assertEquals(new SerialNumber("102000092118"), hub.getSerialNumber()); + assertEquals("My Eco Hub", hub.getName()); + assertEquals(Duration.ofDays(2), hub.getDefaultAwayOverrideLength()); + assertEquals(14, hub.getActiveOverrideId()); + assertEquals("114", hub.getSoftwareVersion()); + assertEquals("11123610_rev._1", hub.getHardwareVersion()); + assertEquals("20190426", hub.getProductionDate()); + } + + @Test + public void testGenerateU03() throws NoboDataException { + Hub hub = Hub.fromH05("V03 102000092118 My Eco Hub 2880 14 114 11123610_rev._1 20190426"); + assertEquals("U03 102000092118 My Eco Hub 2880 14 114 11123610_rev._1 20190426", + hub.generateCommandString("U03")); + } + + @Test + public void testCanChangeOverride() throws NoboDataException { + Hub hub = Hub.fromH05("V03 102000092118 My Eco Hub 2880 14 114 11123610_rev._1 20190426"); + hub.setActiveOverrideId(123); + assertEquals("U03 102000092118 My Eco Hub 2880 123 114 11123610_rev._1 20190426", + hub.generateCommandString("U03")); + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/ModelHelperTest.java b/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/ModelHelperTest.java new file mode 100644 index 00000000000..4a99cf634e5 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/ModelHelperTest.java @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.time.LocalDateTime; +import java.time.Month; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; + +/** + * Unit test for ModelHelper class. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class ModelHelperTest { + + @Test + public void testParseJavaStringNoSpace() { + assertEquals("NoSpace", ModelHelper.toJavaString("NoSpace")); + } + + @Test + public void testParseJavaStringNormalSpace() { + assertEquals("Contains Space", ModelHelper.toJavaString("Contains Space")); + } + + @Test + public void testParseJavaStringNoBreakSpace() { + assertEquals("Contains NoBreak Space", ModelHelper.toJavaString("Contains" + (char) 160 + "NoBreak Space")); + } + + @Test + public void testGenerateNoboStringNoSpace() { + assertEquals("NoSpace", ModelHelper.toHubString("NoSpace")); + } + + @Test + public void testGenerateNoboStringNormalSpace() { + assertEquals("Contains" + (char) 160 + "NoBreak", ModelHelper.toHubString("Contains" + (char) 160 + "NoBreak")); + } + + @Test + public void testGenerateNoboStringNoBreakSpace() { + assertEquals("Contains" + (char) 160 + "NoBreak" + (char) 160 + "Space", + ModelHelper.toHubString("Contains NoBreak Space")); + } + + @Test + public void testParseNull() throws NoboDataException { + assertNull(ModelHelper.toJavaDate("-1")); + } + + @Test + public void testParseDate() throws NoboDataException { + LocalDateTime date = LocalDateTime.of(2020, Month.JANUARY, 22, 19, 30); + assertEquals(date, ModelHelper.toJavaDate("202001221930")); + } + + @Test() + public void testParseIllegalDate() { + assertThrows(NoboDataException.class, () -> ModelHelper.toJavaDate("20201322h1930")); + } + + @Test + public void testGenerateNoboDate() { + LocalDateTime date = LocalDateTime.of(2020, Month.JANUARY, 22, 19, 30); + assertEquals("202001221930", ModelHelper.toHubDateMinutes(date)); + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/OverridePlanRegisterTest.java b/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/OverridePlanRegisterTest.java new file mode 100644 index 00000000000..9085fdec26b --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/OverridePlanRegisterTest.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for OverrideRegister model object. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class OverridePlanRegisterTest { + + @Test + public void testPutGet() throws NoboDataException { + OverridePlan o = OverridePlan.fromH04("H04 4 0 0 -1 -1 0 -1"); + OverrideRegister sut = new OverrideRegister(); + sut.put(o); + assertEquals(o, sut.get(o.getId())); + } + + @Test + public void testPutOverwrite() throws NoboDataException { + OverridePlan o1 = OverridePlan.fromH04("H04 4 0 0 -1 -1 0 -1"); + OverridePlan o2 = OverridePlan.fromH04("H04 4 3 0 -1 -1 0 -1"); + OverrideRegister sut = new OverrideRegister(); + sut.put(o1); + sut.put(o2); + assertEquals(o2, sut.get(o2.getId())); + } + + @Test + public void testRemove() throws NoboDataException { + OverridePlan o = OverridePlan.fromH04("H04 4 0 0 -1 -1 0 -1"); + OverrideRegister sut = new OverrideRegister(); + sut.put(o); + OverridePlan res = sut.remove(o.getId()); + assertEquals(o, res); + } + + @Test + public void testRemoveUnknown() { + OverrideRegister sut = new OverrideRegister(); + OverridePlan res = sut.remove(666); + assertNull(res); + } + + @Test + public void testGetUnknown() { + OverrideRegister sut = new OverrideRegister(); + OverridePlan o = sut.get(666); + assertNull(o); + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/OverridePlanTest.java b/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/OverridePlanTest.java new file mode 100644 index 00000000000..5a02eb4ecf1 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/OverridePlanTest.java @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.time.LocalDateTime; +import java.time.Month; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for Override model object. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class OverridePlanTest { + + @Test + public void testParseH04DefaultOverride() throws NoboDataException { + OverridePlan parsed = OverridePlan.fromH04("H04 4 0 0 -1 -1 0 -1"); + assertEquals(4, parsed.getId()); + assertEquals(OverrideMode.NORMAL, parsed.getMode()); + assertEquals(OverrideType.NOW, parsed.getType()); + assertEquals(OverrideTarget.HUB, parsed.getTarget()); + assertEquals(-1, parsed.getTargetId()); + assertNull(parsed.startTime()); + assertNull(parsed.endTime()); + } + + @Test + public void testParseB03WithStartDate() throws NoboDataException { + OverridePlan parsed = OverridePlan.fromH04("B03 9 3 1 202001221930 -1 0 -1"); + assertEquals(9, parsed.getId()); + assertEquals(OverrideMode.AWAY, parsed.getMode()); + assertEquals(OverrideType.TIMER, parsed.getType()); + assertEquals(OverrideTarget.HUB, parsed.getTarget()); + assertEquals(-1, parsed.getTargetId()); + LocalDateTime date = LocalDateTime.of(2020, Month.JANUARY, 22, 19, 30); + assertEquals(date, parsed.startTime()); + assertNull(parsed.endTime()); + } + + @Test + public void testParseS03NoDate() throws NoboDataException { + OverridePlan parsed = OverridePlan.fromH04("S03 13 0 0 -1 -1 0 -1"); + assertEquals(13, parsed.getId()); + assertEquals(OverrideMode.NORMAL, parsed.getMode()); + assertEquals(OverrideType.NOW, parsed.getType()); + assertEquals(OverrideTarget.HUB, parsed.getTarget()); + assertEquals(-1, parsed.getTargetId()); + assertNull(parsed.startTime()); + assertNull(parsed.endTime()); + } + + @Test + public void testAddA03WithStartDate() throws NoboDataException { + OverridePlan parsed = OverridePlan.fromH04("B03 9 3 1 202001221930 -1 0 -1"); + assertEquals("A03 9 3 1 202001221930 -1 0 -1", parsed.generateCommandString("A03")); + } + + @Test + public void testFromMode() { + LocalDateTime date = LocalDateTime.of(2020, Month.FEBRUARY, 21, 21, 42); + OverridePlan overridePlan = OverridePlan.fromMode(OverrideMode.AWAY, date); + assertEquals("A03 1 3 0 -1 -1 0 -1", overridePlan.generateCommandString("A03")); + } + + @Test + public void testModeNames() throws NoboDataException { + assertEquals(OverrideMode.AWAY, OverrideMode.getByName("Away")); + assertEquals(OverrideMode.ECO, OverrideMode.getByName("ECO")); + assertEquals(OverrideMode.NORMAL, OverrideMode.getByName("Normal")); + assertEquals(OverrideMode.COMFORT, OverrideMode.getByName("COMFORT")); + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/SerialNumberTest.java b/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/SerialNumberTest.java new file mode 100644 index 00000000000..cff376a4b77 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/SerialNumberTest.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for serial number model object. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class SerialNumberTest { + + @Test + public void testIsWellFormed() { + assertTrue(new SerialNumber("123123123123").isWellFormed()); + assertFalse(new SerialNumber("123123123").isWellFormed()); + assertFalse(new SerialNumber("123 123 123 123").isWellFormed()); + assertFalse(new SerialNumber("123123123xyz").isWellFormed()); + assertFalse(new SerialNumber("123123123987").isWellFormed()); + } + + @Test + public void testGetTypeIdentifier() { + assertEquals("123", new SerialNumber("123123123123").getTypeIdentifier()); + assertEquals("Unknown", new SerialNumber("xyz").getTypeIdentifier()); + } + + @Test + public void testGetComponentType() { + assertEquals("NTD-4R", new SerialNumber("186170024143").getComponentType()); + assertEquals("Nobø Switch", new SerialNumber("234001021010").getComponentType()); + assertEquals("Unknown, please contact maintainer to add a new type for 123123123123", + new SerialNumber("123123123123").getComponentType()); + assertEquals("Unknown, please contact maintainer to add a new type for foobar", + new SerialNumber("foobar").getComponentType()); + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/TemperatureTest.java b/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/TemperatureTest.java new file mode 100644 index 00000000000..6f04b590e2b --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/TemperatureTest.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for temperature model object. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class TemperatureTest { + + @Test + public void testParseY02() throws NoboDataException { + Temperature temp = Temperature.fromY02("Y02 123123123123 12.345"); + assertEquals(new SerialNumber("123123123123"), temp.getSerialNumber()); + assertEquals(12.34, temp.getTemperature(), 0.1); + } + + @Test + public void testParseY02NATemp() throws NoboDataException { + Temperature temp = Temperature.fromY02("Y02 123123123123 N/A"); + assertEquals(new SerialNumber("123123123123"), temp.getSerialNumber()); + assertEquals(Double.NaN, temp.getTemperature(), 0.1); + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/WeekProfileRegisterTest.java b/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/WeekProfileRegisterTest.java new file mode 100644 index 00000000000..1ce895f0eb8 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/WeekProfileRegisterTest.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for WeekProfileRegister model object. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class WeekProfileRegisterTest { + + @Test + public void testPutGet() throws NoboDataException { + WeekProfile p1 = WeekProfile.fromH03( + "H03 1 Default 00000,06001,08000,15001,23000,00000,06001,08000,15001,23000,00000,06001,08000,15001,23000,00000,06001,08000,15001,23000,00000,06001,08000,15001,00000,07001,00000,07001,23000"); + WeekProfileRegister sut = new WeekProfileRegister(); + sut.put(p1); + assertEquals(p1, sut.get(p1.getId())); + } + + @Test + public void testPutOverwrite() throws NoboDataException { + WeekProfile p1 = WeekProfile.fromH03( + "H03 1 Default 00000,06001,08000,15001,23000,00000,06001,08000,15001,23000,00000,06001,08000,15001,23000,00000,06001,08000,15001,23000,00000,06001,08000,15001,00000,07001,00000,07001,23000"); + WeekProfile p2 = WeekProfile.fromH03( + "H03 2 HomeOffice 00000,06001,09000,15001,23000,00000,06001,08000,15001,23000,00000,06001,08000,15001,23000,00000,06001,08000,15001,23000,00000,06001,08000,15001,00000,07001,00000,07001,23000"); + WeekProfileRegister sut = new WeekProfileRegister(); + sut.put(p1); + sut.put(p2); + assertEquals(p2, sut.get(p2.getId())); + } + + @Test + public void testRemove() throws NoboDataException { + WeekProfile p1 = WeekProfile.fromH03( + "H03 1 Default 00000,06001,08000,15001,23000,00000,06001,08000,15001,23000,00000,06001,08000,15001,23000,00000,06001,08000,15001,23000,00000,06001,08000,15001,00000,07001,00000,07001,23000"); + WeekProfileRegister sut = new WeekProfileRegister(); + sut.put(p1); + WeekProfile res = sut.remove(p1.getId()); + assertEquals(p1, res); + } + + @Test + public void testRemoveUnknown() { + WeekProfileRegister sut = new WeekProfileRegister(); + WeekProfile res = sut.remove(666); + assertEquals(null, res); + } + + @Test + public void testGetUnknown() { + WeekProfileRegister sut = new WeekProfileRegister(); + WeekProfile o = sut.get(666); + assertEquals(null, o); + } + + @Test + public void testIsEmpty() throws NoboDataException { + WeekProfile p1 = WeekProfile.fromH03( + "H03 1 Default 00000,06001,08000,15001,23000,00000,06001,08000,15001,23000,00000,06001,08000,15001,23000,00000,06001,08000,15001,23000,00000,06001,08000,15001,00000,07001,00000,07001,23000"); + WeekProfileRegister sut = new WeekProfileRegister(); + assertEquals(true, sut.isEmpty()); + sut.put(p1); + assertEquals(false, sut.isEmpty()); + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/WeekProfileTest.java b/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/WeekProfileTest.java new file mode 100644 index 00000000000..88c9a73c94f --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/WeekProfileTest.java @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal.model; + +import java.time.LocalDateTime; +import java.time.Month; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for WeekProfile model object. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class WeekProfileTest { + + private static final LocalDateTime MONDAY = LocalDateTime.of(2020, Month.MAY, 11, 0, 0); + private static final LocalDateTime WEDNESDAY = LocalDateTime.of(2020, Month.MAY, 13, 0, 0); + private static final LocalDateTime SUNDAY = LocalDateTime.of(2020, Month.MAY, 17, 23, 59); + + @Test + public void testParseH03() throws NoboDataException { + WeekProfile weekProfile = WeekProfile.fromH03( + "H03 1 Default 00000,06001,08000,15001,23000,00000,06001,08000,15001,23000,00000,06001,08000,15001,23000,00000,06001,08000,15001,23000,00000,06001,08000,15001,00000,07001,00000,07001,23000"); + Assertions.assertEquals(1, weekProfile.getId()); + Assertions.assertEquals("Default", weekProfile.getName()); + } + + @Test + public void testFindFirstStatus() throws NoboDataException { + WeekProfile weekProfile = WeekProfile.fromH03( + "H03 1 Default 00000,06001,08000,15001,23000,00000,06001,08000,15001,23000,00000,06001,08000,15001,23000,00000,06001,08000,15001,23000,00000,06001,08000,15001,00000,07001,00000,07001,23000"); + WeekProfileStatus status = weekProfile.getStatusAt(MONDAY); + Assertions.assertEquals(WeekProfileStatus.ECO, status); + } + + @Test + public void testFindLastStatus() throws NoboDataException { + WeekProfile weekProfile = WeekProfile.fromH03( + "H03 1 Default 00000,06001,08000,15001,23000,00000,06001,08000,15001,23000,00000,06001,08000,15001,23000,00000,06001,08000,15001,23000,00000,06001,08000,15001,00000,07001,00000,07001,23000"); + WeekProfileStatus status = weekProfile.getStatusAt(SUNDAY); + Assertions.assertEquals(WeekProfileStatus.ECO, status); + } + + @Test + public void testFindEmptyDayStatus() throws NoboDataException { + WeekProfile weekProfile = WeekProfile.fromH03("H03 1 Default 00000,00000,00001,00000,00000,00000,00000"); + WeekProfileStatus status = weekProfile.getStatusAt(WEDNESDAY); + Assertions.assertEquals(WeekProfileStatus.COMFORT, status); + } + + @Test + public void testFindOffDayStatus() throws NoboDataException { + WeekProfile weekProfile = WeekProfile.fromH03("H03 2 Off 00004,00003,00004,00004,00004,00004,00003"); + WeekProfileStatus statusWen = weekProfile.getStatusAt(WEDNESDAY); + Assertions.assertEquals(WeekProfileStatus.OFF, statusWen); + WeekProfileStatus statusSat = weekProfile.getStatusAt(SUNDAY); + Assertions.assertEquals(WeekProfileStatus.OFF, statusSat); + } + + @Test + public void testFindStartingNowStatus() throws NoboDataException { + WeekProfile weekProfile = WeekProfile.fromH03( + "H03 1 Default 00000,06001,08000,15001,23000,00000,06001,08000,15001,23000,00000,06001,08000,15001,23000,00000,06001,08000,15001,23000,00000,06001,08000,15001,00000,07001,00000,07001,23000"); + WeekProfileStatus status = weekProfile.getStatusAt(MONDAY.plusHours(6)); + Assertions.assertEquals(WeekProfileStatus.COMFORT, status); + + status = weekProfile.getStatusAt(MONDAY.plusHours(6).plusMinutes(1)); + Assertions.assertEquals(WeekProfileStatus.COMFORT, status); + + status = weekProfile.getStatusAt(MONDAY.plusHours(6).minusMinutes(1)); + Assertions.assertEquals(WeekProfileStatus.ECO, status); + } + + @Test + public void testFindNormalStatus() throws NoboDataException { + WeekProfile weekProfile = WeekProfile.fromH03( + "H03 1 Default 00000,06001,08000,15001,23000,00000,06001,08000,15001,23000,00000,06001,08000,15001,23000,00000,06001,08000,15001,23000,00000,06001,08000,15001,00000,07001,00000,07001,23000"); + WeekProfileStatus status = weekProfile.getStatusAt(WEDNESDAY.plusHours(7).plusMinutes(13)); + Assertions.assertEquals(WeekProfileStatus.COMFORT, status); + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/ZoneRegisterTest.java b/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/ZoneRegisterTest.java new file mode 100644 index 00000000000..d667ffc7d3a --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/ZoneRegisterTest.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for ZoneRegister model object. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class ZoneRegisterTest { + + @Test + public void testPutGet() throws NoboDataException { + Zone z = Zone.fromH01("H01 1 1. etage 20 22 16 1 -1"); + ZoneRegister sut = new ZoneRegister(); + sut.put(z); + assertEquals(z, sut.get(z.getId())); + } + + @Test + public void testPutOverwrite() throws NoboDataException { + Zone z1 = Zone.fromH01("H01 1 1. etage 20 22 16 1 -1"); + Zone z2 = Zone.fromH01("H01 1 2. etage 20 22 16 1 -1"); + ZoneRegister sut = new ZoneRegister(); + sut.put(z1); + sut.put(z2); + assertEquals(z2, sut.get(z2.getId())); + } + + @Test + public void testRemove() throws NoboDataException { + Zone z = Zone.fromH01("H01 1 1. etage 20 22 16 1 -1"); + ZoneRegister sut = new ZoneRegister(); + sut.put(z); + Zone res = sut.remove(z.getId()); + assertEquals(z, res); + } + + @Test + public void testRemoveUnknown() { + ZoneRegister sut = new ZoneRegister(); + Zone res = sut.remove(666); + assertEquals(null, res); + } + + @Test + public void testGetUnknown() { + ZoneRegister sut = new ZoneRegister(); + Zone z = sut.get(666); + assertEquals(null, z); + } + + @Test + public void testValues() throws NoboDataException { + Zone z1 = Zone.fromH01("H01 1 1. etage 20 22 16 1 -1"); + Zone z2 = Zone.fromH01("H01 2 2. etage 20 22 16 1 -1"); + ZoneRegister sut = new ZoneRegister(); + sut.put(z1); + sut.put(z2); + assertEquals(2, sut.values().size()); + assertEquals(true, sut.values().contains(z1)); + assertEquals(true, sut.values().contains(z2)); + } +} diff --git a/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/ZoneTest.java b/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/ZoneTest.java new file mode 100644 index 00000000000..e4775f06994 --- /dev/null +++ b/bundles/org.openhab.binding.nobohub/src/test/java/org/openhab/binding/nobohub/internal/model/ZoneTest.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2010-2022 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.nobohub.internal.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for Zone model object. + * + * @author Jørgen Austvik - Initial contribution + */ +@NonNullByDefault +public class ZoneTest { + + @Test + public void testParseH01Simple() throws NoboDataException { + Zone zone = Zone.fromH01("H01 1 1. etage 20 22 16 1 -1"); + assertEquals(1, zone.getId()); + assertEquals("1. etage", zone.getName()); + assertEquals(20, zone.getActiveWeekProfileId()); + assertTrue(zone.getAllowOverrides()); + assertEquals(16, zone.getEcoTemperature()); + assertEquals(22, zone.getComfortTemperature()); + } + + @Test + public void testGenerateCommand() throws NoboDataException { + Zone zone = Zone.fromH01("H01 1 1. etage 20 22 16 1 -1"); + assertEquals("U00 1 1. etage 20 22 16 1 -1", zone.generateCommandString("U00")); + } +} diff --git a/bundles/pom.xml b/bundles/pom.xml index 2df26d5a388..61df9d6e0ce 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -256,6 +256,7 @@ org.openhab.binding.nibeuplink org.openhab.binding.nikobus org.openhab.binding.nikohomecontrol + org.openhab.binding.nobohub org.openhab.binding.novafinedust org.openhab.binding.ntp org.openhab.binding.nuki