[nobohub] Initial contribution (#12937)

* Added NoboHub binding.

Signed-off-by: Espen Fossen <espenaf@junta.no>
This commit is contained in:
Espen Fossen 2022-08-22 23:27:24 +02:00 committed by GitHub
parent 9c2070f748
commit bc9cf8e07a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 4828 additions and 0 deletions

View File

@ -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

View File

@ -1111,6 +1111,11 @@
<artifactId>org.openhab.binding.nikohomecontrol</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.nobohub</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.novafinedust</artifactId>

View File

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

View File

@ -0,0 +1,167 @@
# NoboHub Binding
This binding controls the Glen Dimplex Nobø Hub using the <a href="https://www.glendimplex.se/media/15650/nobo-hub-api-v-1-1-integration-for-advanced-users.pdf">Nobø Hub API v1.1</a>.
![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" <heating> {channel="nobohub:nobohub:controller:activeOverrideName"}
// Panel Heater
Number:Temperature PanelHeater_CurrentTemperature "Setpoint [%.1f °C]" <temperature> {channel="nobohub:component:controller:184000000000:currentTemperature"}
// Zone
String Zone_ActiveWeekProfileName "Active week profile name [%s]" <calendar> {channel="nobohub:zone:controller:1:activeWeekProfileName"}
Number Zone_ActiveWeekProfile "Active week profile [%d]" <calendar> {channel="nobohub:zone:controller:1:activeWeekProfile"}
String Zone_ActiveStatus "Active status %s]" <heating> {channel="nobohub:zone:controller:1:calculatedWeekProfileStatus"}
Number:Temperature Zone_ComfortTemperature "Comfort temperature [%.1f °C]" <temperature> {channel="nobohub:zone:controller:1:comfortTemperature"}
Number:Temperature Zone_EcoTemperatur "Eco temperature [%.1f °C]" <temperature> {channel="nobohub:zone:controller:1:ecoTemperature"}
Number:Temperature Zone_CurrentTemperature "Current temperature [%.1f °C]" <temperature> {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
}
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

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

View File

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

View File

@ -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;
}

View File

@ -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<Temperature> currentTemperature = new QuantityType<>(temp, SIUnits.CELSIUS);
updateState(CHANNEL_COMPONENT_CURRENT_TEMPERATURE, currentTemperature);
}
Map<String, String> 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;
}
}

View File

@ -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;
}
}

View File

@ -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<ThingTypeUID> AUTODISCOVERED_THING_TYPES_UIDS = new HashSet<>(
Arrays.asList(THING_TYPE_ZONE, THING_TYPE_COMPONENT));
public static final Set<ThingTypeUID> 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<String, String> 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::<String, String> unmodifiableMap));
// Full list of units: https://help.nobo.no/skriver/?chapterid=344&chapterlanguageid=2
public static final Map<String, String> 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::<String, String> unmodifiableMap));
}

View File

@ -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;
}

View File

@ -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<String, String> 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<WeekProfile> getWeekProfiles() {
return weekProfileRegister.values();
}
public void setStatusInfo(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
updateStatus(status, statusDetail, description);
}
}

View File

@ -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;
}

View File

@ -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<ThingTypeUID, ServiceRegistration<?>> discoveryServiceRegs = new HashMap<>();
public static final Set<ThingTypeUID> 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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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<Temperature> comfortTemperature = new QuantityType<>(zone.getComfortTemperature(),
SIUnits.CELSIUS);
updateState(CHANNEL_ZONE_COMFORT_TEMPERATURE, comfortTemperature);
QuantityType<Temperature> ecoTemperature = new QuantityType<>(zone.getEcoTemperature(), SIUnits.CELSIUS);
updateState(CHANNEL_ZONE_ECO_TEMPERATURE, ecoTemperature);
Double temp = zone.getTemperature();
if (temp != null && !Double.isNaN(temp)) {
QuantityType<Temperature> 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<StateOption> 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<String, String> 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;
}
}

View File

@ -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());
}
}
}

View File

@ -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);
}
}

View File

@ -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<String, Object> 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);
}
};
}

View File

@ -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<Zone> zones) {
ThingUID bridge = bridgeHandler.getThing().getUID();
List<Thing> 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<String, Object> 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<Component> components) {
ThingUID bridge = bridgeHandler.getThing().getUID();
List<Thing> 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<String, Object> 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();
}
}

View File

@ -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;
}
}

View File

@ -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<SerialNumber, Component> register = new HashMap<SerialNumber, Component>();
/**
* 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<Component> values() {
return register.values();
}
}

View File

@ -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;
}
}

View File

@ -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";
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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));
}
}

View File

@ -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;
}
}

View File

@ -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<Integer, OverridePlan> 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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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<String> 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();
}
}

View File

@ -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;
}
}

View File

@ -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));
}
}

View File

@ -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<Integer, WeekProfile> register = new HashMap<Integer, WeekProfile>();
/**
* 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<WeekProfile> values() {
return register.values();
}
public boolean isEmpty() {
return register.isEmpty();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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<Integer, Zone> register = new HashMap<Integer, Zone>();
/**
* 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<Zone> values() {
return register.values();
}
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="nobohub" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Glen Dimplex Nobø Hub Binding</name>
<description>This is the binding for Glen Dimplex Nobø Hub.</description>
</binding:binding>

View File

@ -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}

View File

@ -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}

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="nobohub"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="nobohub">
<label>Nobo Hub</label>
<description>Nobo Hub Bridge Binding</description>
<channels>
<channel id="activeOverrideName" typeId="activeOverrideName-channel-type"/>
<channel id="weekProfiles" typeId="weekProfiles-channel-type"/>
</channels>
<properties>
<property name="vendor">Glen Dimplex Nobo</property>
</properties>
<representation-property>serialNumber</representation-property>
<config-description>
<parameter name="serialNumber" type="text" required="true">
<label>Serial Number</label>
<description>Serial number of the Nobo hub (12 numbers, no spaces)</description>
</parameter>
<parameter name="hostName" type="text" required="true">
<label>Host Name</label>
<description>Host Name/IP address of the Nobo Hub</description>
</parameter>
<parameter name="keepaliveInterval" type="integer" required="false" min="5">
<label>Polling interval</label>
<description>Polling interval (seconds). Default: 14</description>
<default>14</default>
</parameter>
</config-description>
</bridge-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,137 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="nobohub"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="zone">
<supported-bridge-type-refs>
<bridge-type-ref id="nobohub"/>
</supported-bridge-type-refs>
<label>Zone</label>
<description>A zone can contain several Nobo devices</description>
<channels>
<channel id="activeWeekProfileName" typeId="activeWeekProfileName-channel-type"/>
<channel id="activeWeekProfile" typeId="activeWeekProfile-channel-type"/>
<channel id="comfortTemperature" typeId="comfort-temperature-channel-type"/>
<channel id="ecoTemperature" typeId="eco-temperature-channel-type"/>
<channel id="currentTemperature" typeId="temperature-channel-type"/>
<channel id="calculatedWeekProfileStatus" typeId="activeOverrideName-channel-type"/>
</channels>
<properties>
<property name="vendor">Glen Dimplex Nobo</property>
</properties>
<representation-property>name</representation-property>
<config-description>
<parameter name="id" type="integer" required="true">
<label>Id</label>
<description>Id of the Zone</description>
</parameter>
</config-description>
</thing-type>
<thing-type id="component">
<supported-bridge-type-refs>
<bridge-type-ref id="nobohub"/>
</supported-bridge-type-refs>
<label>Component</label>
<description>A component is an oven, a switch or a floor thermostat</description>
<channels>
<channel id="currentTemperature" typeId="temperature-channel-type"/>
</channels>
<properties>
<property name="vendor">Glen Dimplex Nobo</property>
</properties>
<representation-property>serialNumber</representation-property>
<config-description>
<parameter name="serialNumber" type="text" required="true">
<label>Serial Number</label>
<description>Serial number of the component (12 digits)</description>
</parameter>
</config-description>
</thing-type>
<channel-type id="activeOverrideName-channel-type">
<item-type>String</item-type>
<label>Active Override</label>
<description>Name of active override, using one of the predefined states supported</description>
<category>Heating</category>
<state readOnly="false">
<options>
<option value="NORMAL">Normal</option>
<option value="COMFORT">Comfort</option>
<option value="ECO">Eco</option>
<option value="Away">Away</option>
</options>
</state>
</channel-type>
<channel-type id="eco-temperature-channel-type">
<item-type>Number:Temperature</item-type>
<label>Eco Temperature</label>
<description>The preferred Eco temperature level set on the heater or in the binding</description>
<category>Temperature</category>
<tags>
<tag>Setpoint</tag>
<tag>Temperature</tag>
</tags>
<state min="7" max="30" step="1" pattern="%d °C" readOnly="false"/>
</channel-type>
<channel-type id="comfort-temperature-channel-type">
<item-type>Number:Temperature</item-type>
<label>Comfort Temperature</label>
<description>The preferred Comfort temperature level set on the heater or in the binding</description>
<category>Temperature</category>
<tags>
<tag>Setpoint</tag>
<tag>Temperature</tag>
</tags>
<state min="7" max="30" step="1" pattern="%d °C" readOnly="false"/>
</channel-type>
<channel-type id="temperature-channel-type">
<item-type>Number:Temperature</item-type>
<label>Current Temperature</label>
<description>The current temperature from a device that supports reporting temperatures</description>
<category>Temperature</category>
<tags>
<tag>Measurement</tag>
<tag>Temperature</tag>
</tags>
<state pattern="%.3f °C" readOnly="true"/>
</channel-type>
<channel-type id="activeWeekProfileName-channel-type">
<item-type>String</item-type>
<label>Active Week Profile Name</label>
<description>Name of the active week profile, set via the Nobo app</description>
<category>Heating</category>
<state readOnly="true"/>
</channel-type>
<channel-type id="activeWeekProfile-channel-type">
<item-type>Number</item-type>
<label>Active Week Profile Id</label>
<description>Id of the active week profile, set via the Nobo app</description>
<category>Heating</category>
<state min="0" readOnly="false"/>
</channel-type>
<channel-type id="weekProfiles-channel-type">
<item-type>String</item-type>
<label>Week Profiles</label>
<description>List of active week profiles, set via the Nobo app</description>
<category>Heating</category>
<state readOnly="true"/>
</channel-type>
</thing:thing-descriptions>

View File

@ -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));
}
}

View File

@ -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"));
}
}

View File

@ -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"));
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}

View File

@ -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"));
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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));
}
}

View File

@ -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"));
}
}

View File

@ -256,6 +256,7 @@
<module>org.openhab.binding.nibeuplink</module>
<module>org.openhab.binding.nikobus</module>
<module>org.openhab.binding.nikohomecontrol</module>
<module>org.openhab.binding.nobohub</module>
<module>org.openhab.binding.novafinedust</module>
<module>org.openhab.binding.ntp</module>
<module>org.openhab.binding.nuki</module>