From 66ab135befe53855d0d1b817473b6ff252c129ae Mon Sep 17 00:00:00 2001 From: Mark Herwege Date: Mon, 22 Sep 2025 22:43:26 +0200 Subject: [PATCH 01/13] car chargers Signed-off-by: Mark Herwege --- .../README.md | 30 +- .../NikoHomeControlBindingConstants.java | 22 +- .../NikoHomeControlHandlerFactory.java | 4 + .../NikoHomeControlDiscoveryService.java | 15 + .../NikoHomeControlCarChargerConfig.java | 25 ++ .../NikoHomeControlCarChargerHandler.java | 291 +++++++++++++++ .../internal/protocol/NhcCarCharger.java | 351 ++++++++++++++++++ .../internal/protocol/NhcCarChargerEvent.java | 55 +++ .../NikoHomeControlCommunication.java | 41 ++ .../protocol/NikoHomeControlConstants.java | 36 ++ .../protocol/nhc2/NhcCarCharger2.java | 62 ++++ .../internal/protocol/nhc2/NhcDevice2.java | 36 ++ .../nhc2/NikoHomeControlCommunication2.java | 161 ++++++++ .../OH-INF/i18n/nikohomecontrol.properties | 41 ++ .../resources/OH-INF/thing/thing-types.xml | 123 ++++++ 15 files changed, 1282 insertions(+), 11 deletions(-) create mode 100644 bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlCarChargerConfig.java create mode 100644 bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlCarChargerHandler.java create mode 100644 bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcCarCharger.java create mode 100644 bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcCarChargerEvent.java create mode 100644 bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcCarCharger2.java diff --git a/bundles/org.openhab.binding.nikohomecontrol/README.md b/bundles/org.openhab.binding.nikohomecontrol/README.md index 59dbafecb8d86..948b851049311 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/README.md +++ b/bundles/org.openhab.binding.nikohomecontrol/README.md @@ -24,7 +24,7 @@ The installation only needs to be 'connected' (registered on the Niko Home Contr For Niko Home Control I, the binding exposes all actions from the Niko Home Control System that can be triggered from the smartphone/tablet interface, as defined in the Niko Home Control I programming software. For Niko Home Control II, the binding exposes all devices in the system. -Supported device types are switches, dimmers and rollershutters or blinds, thermostats, energy meters (Niko Home Control I only) and access control (Niko Home Control II only). +Supported device types are switches, dimmers and rollershutters or blinds, thermostats, energy meters (Niko Home Control I only), access control (Niko Home Control II only) and car chargers (Niko Home Control II only). Niko Home Control alarm and notice messages are retrieved and made available in the binding. ## Supported Things @@ -48,6 +48,7 @@ The following thing types are available in the binding: | access | | x | door with bell button and lock | | accessRingAndComeIn | | x | door with bell button, lock and ring and come in functionality | | alarm | | x | alarm system | +| carCharger | x | x | car charger device | ## Binding Configuration @@ -128,7 +129,7 @@ The `password` parameter should be set to the profile password. ## Thing Configuration -The Thing configurations for **Niko Home Control actions, thermostats, energy meters and access devices** have the following parameters: +The Thing configurations for **Niko Home Control actions, thermostats, energy meters, access devices and car chargers** have the following parameters: | Parameter | NHC I | NHC II | Required | Thing Types | Description | |---------------|:-----:|:------:|:--------:|----------------------------------|-----------------------------------------------------------------------------------| @@ -141,6 +142,7 @@ The Thing configurations for **Niko Home Control actions, thermostats, energy me | refresh | x | x | | energyMeterLive, energyMeter, gasMeter, waterMeter | refresh interval for meter reading in minutes, default 10 minutes. The value should not be lower than 5 minutes to avoid too many meter data retrieval calls | | accessId | | x | x | access, accessRingAndComeIn | unique ID for the access device in the controller | | alarmId | | x | x | alarm | unique ID for the alarm system in the controller | +| carChargerId | | x | x | car charger | unique ID for the car charger in the controller | For Niko Home Control I, the `actionId`, `thermostatId` or `meterId` parameter are the unique IP Interface Object ID (`ipInterfaceObjectId`) as automatically assigned in the Niko Home Control Controller when programming the Niko Home Control system using the Niko Home Control I programming software. It is not directly visible in the Niko Home Control programming or user software, but will be detected and automatically set by openHAB discovery. @@ -151,11 +153,8 @@ For Niko Home Control II, the `actionId` parameter is a unique ID for the action It can only be auto-discovered. If you want to define the action through textual configuration, the easiest way is to first do discovery on the bridge to get the correct `actionId` to use in the textual configuration. Discover and add the thing you want to add. -Note down the `actionId` parameter from the thing, remove it before adding it again through textual configuration, with the same `actionId` parameter. -Alternatively the `actionId` can be retrieved from the configuration file. -The file contains a SQLLite database. -The database contains a table `Action` with column `FifthplayId` corresponding to the required `actionId` parameter. -The same applies applies for `thermostatId`, `meterId`, `accessId` and `alarmId`. +You can directly create the textual configuration for the discovered thing in the UI. +The same applies applies for `thermostatId`, `meterId`, `accessId`, `alarmId` and `carChargerId`. An example **action** textual configuration looks like: @@ -187,6 +186,12 @@ For **alarm systems**: Thing nikohomecontrol:alarm:mybridge:myalarm [ alarmId="abcdef01-dcba-1234-ab98-012345abcdef" ] ``` +For **car chargers**: + +```java +Thing nikohomecontrol:carCharger:mybridge:mycarcharger [ carChargerId="abcdef01-dcba-1234-ab98-012345abcdef" ] +``` + ## Channels | Channel Type ID | RW | Advanced | Item Type | Thing Types | Description | @@ -216,6 +221,17 @@ Thing nikohomecontrol:alarm:mybridge:myalarm [ alarmId="abcdef01-dcba-1234-ab98- | arm | RW | | Switch | alarm | arm/disarm alarm, will change state (on/off) immediately. Note some integrations (Homekit, Google Home, ...) may require String states for an alarm system (ARMED/DISARMED). This can be achieved using an extra item and a rule updated by/commanding an item linked to this channel | | armed | RW | | Switch | alarm | state of the alarm system (on/off), will only turn on after pre-armed period when arming | | state | R | | String | alarm | state of the alarm system (DISARMED, PREARMED, ARMED, PREALARM, ALARM, DETECTOR PROBLEM) | +| status | RW | | Switch | carCharger | status of the car charger (on/off) | +| chargingStatus | R | | String | carCharger | charging status of the car charger (ACTIVE, INACTIVE, BATTERY FULL or ERROR) | +| evStatus | R | | String | carCharger | status of the electric vehicle (IDLE, CONNECTED or CHARGING) | +| couplingStatus | R | | String | carCharger | coupling status (OK, NO INTERNET, NO CREDENTIALS, INVALID CREDENTIALS, CONNECTION ERROR, CONNECTION TIMEOUT, API ERROR or UNKNOWN ERROR) | +| electricalPower | R | | Number:Power | carCharger | current charging power | +| chargingMode | RW | | String | carCharger | charging mode (SOLAR, NORMAL or SMART) | +| targetDistance | RW | | Number:Length | carCharger | target distance to achieve in charging activity | +| targetTime | RW | | DateTime | carCharger | time by which the target distance should be achieved | +| boost | RW | | Switch | carCharger | boost charging to maximum achievable, not respecting capacity limit | +| reachableDistance | R | | Number:Length | carCharger | reachable distance in current charing activity | +| nextChargingTime | R | | DateTime | carCharger | next charging start in current charging activity | | alarm | | | | bridge, alarm | trigger channel with alarm event message, can be used in rules | | notice | | | | bridge | trigger channel with notice event message, can be used in rules | diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/NikoHomeControlBindingConstants.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/NikoHomeControlBindingConstants.java index f26f76b14b43b..0e94cf6a8f35d 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/NikoHomeControlBindingConstants.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/NikoHomeControlBindingConstants.java @@ -53,6 +53,7 @@ public class NikoHomeControlBindingConstants { public static final ThingTypeUID THING_TYPE_ACCESS_RINGANDCOMEIN = new ThingTypeUID(BINDING_ID, "accessRingAndComeIn"); public static final ThingTypeUID THING_TYPE_ALARM = new ThingTypeUID(BINDING_ID, "alarm"); + public static final ThingTypeUID THING_TYPE_CAR_CHARGER = new ThingTypeUID(BINDING_ID, "carCharger"); // thing type sets public static final Set BRIDGE_THING_TYPES_UIDS = Set.of(BRIDGEI_THING_TYPE, BRIDGEII_THING_TYPE); @@ -64,10 +65,10 @@ public class NikoHomeControlBindingConstants { public static final Set ACCESS_THING_TYPES_UIDS = Set.of(THING_TYPE_ACCESS, THING_TYPE_ACCESS_RINGANDCOMEIN); public static final Set ALARM_THING_TYPES_UIDS = Set.of(THING_TYPE_ALARM); - public static final Set SUPPORTED_THING_TYPES_UIDS = Stream - .of(BRIDGE_THING_TYPES_UIDS.stream(), ACTION_THING_TYPES_UIDS.stream(), - THERMOSTAT_THING_TYPES_UIDS.stream(), METER_THING_TYPES_UIDS.stream(), - ACCESS_THING_TYPES_UIDS.stream(), ALARM_THING_TYPES_UIDS.stream()) + public static final Set CAR_CHARGER_THING_TYPES_UIDS = Set.of(THING_TYPE_CAR_CHARGER); + public static final Set SUPPORTED_THING_TYPES_UIDS = Stream.of(BRIDGE_THING_TYPES_UIDS.stream(), + ACTION_THING_TYPES_UIDS.stream(), THERMOSTAT_THING_TYPES_UIDS.stream(), METER_THING_TYPES_UIDS.stream(), + ACCESS_THING_TYPES_UIDS.stream(), ALARM_THING_TYPES_UIDS.stream(), CAR_CHARGER_THING_TYPES_UIDS.stream()) .flatMap(i -> i).collect(Collectors.toSet()); // List of all Channel ids @@ -103,6 +104,17 @@ public class NikoHomeControlBindingConstants { public static final String CHANNEL_ARMED = "armed"; public static final String CHANNEL_STATE = "state"; + public static final String CHANNEL_STATUS = "status"; + public static final String CHANNEL_CHARGING_STATUS = "chargingStatus"; + public static final String CHANNEL_EV_STATUS = "evStatus"; + public static final String CHANNEL_COUPLING_STATUS = "couplingStatus"; + public static final String CHANNEL_CHARGING_MODE = "chargingMode"; + public static final String CHANNEL_TARGET_DISTANCE = "targetDistance"; + public static final String CHANNEL_TARGET_TIME = "targetTime"; + public static final String CHANNEL_BOOST = "boost"; + public static final String CHANNEL_REACHABLE_DISTANCE = "reachableDistance"; + public static final String CHANNEL_NEXT_CHARGING_TIME = "nextChargingTime"; + public static final String CHANNEL_ALARM = "alarm"; public static final String CHANNEL_NOTICE = "notice"; @@ -129,6 +141,8 @@ public class NikoHomeControlBindingConstants { public static final String CONFIG_ALARM_ID = "alarmId"; + public static final String CONFIG_CAR_CHARGER_ID = "carChargerId"; + // Thing properties public static final String PROPERTY_DEVICE_TYPE = "deviceType"; public static final String PROPERTY_DEVICE_TECHNOLOGY = "deviceTechnology"; diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/NikoHomeControlHandlerFactory.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/NikoHomeControlHandlerFactory.java index fee79dd10f2c2..cf2b200aad35d 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/NikoHomeControlHandlerFactory.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/NikoHomeControlHandlerFactory.java @@ -21,6 +21,7 @@ import org.openhab.binding.nikohomecontrol.internal.handler.NikoHomeControlAlarmHandler; import org.openhab.binding.nikohomecontrol.internal.handler.NikoHomeControlBridgeHandler1; import org.openhab.binding.nikohomecontrol.internal.handler.NikoHomeControlBridgeHandler2; +import org.openhab.binding.nikohomecontrol.internal.handler.NikoHomeControlCarChargerHandler; import org.openhab.binding.nikohomecontrol.internal.handler.NikoHomeControlMeterHandler; import org.openhab.binding.nikohomecontrol.internal.handler.NikoHomeControlThermostatHandler; import org.openhab.core.i18n.TimeZoneProvider; @@ -40,6 +41,7 @@ * handlers. * * @author Mark Herwege - Initial Contribution + * @author Mark Herwege - Add car chargers */ @NonNullByDefault @@ -79,6 +81,8 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { return new NikoHomeControlAccessHandler(thing); } else if (ALARM_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { return new NikoHomeControlAlarmHandler(thing); + } else if (CAR_CHARGER_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { + return new NikoHomeControlCarChargerHandler(thing); } return null; diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/discovery/NikoHomeControlDiscoveryService.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/discovery/NikoHomeControlDiscoveryService.java index 24cc86a6976d3..f8232e35dd941 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/discovery/NikoHomeControlDiscoveryService.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/discovery/NikoHomeControlDiscoveryService.java @@ -25,6 +25,7 @@ import org.openhab.binding.nikohomecontrol.internal.protocol.NhcAccess; import org.openhab.binding.nikohomecontrol.internal.protocol.NhcAction; import org.openhab.binding.nikohomecontrol.internal.protocol.NhcAlarm; +import org.openhab.binding.nikohomecontrol.internal.protocol.NhcCarCharger; import org.openhab.binding.nikohomecontrol.internal.protocol.NhcMeter; import org.openhab.binding.nikohomecontrol.internal.protocol.NhcThermostat; import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlCommunication; @@ -41,6 +42,7 @@ * {@link NikoHomeControlDiscoveryService} is used to return Niko Home Control Actions as things to the framework. * * @author Mark Herwege - Initial Contribution + * @author Mark Herwege - Add car chargers */ @Component(scope = ServiceScope.PROTOTYPE, service = NikoHomeControlDiscoveryService.class) @NonNullByDefault @@ -85,6 +87,7 @@ public void discoverDevices() { discoverMeterDevices(thingHandler, nhcComm); discoverAccessDevices(thingHandler, nhcComm); discoverAlarmDevices(thingHandler, nhcComm); + discoverCarChargerDevices(thingHandler, nhcComm); } private void discoverActionDevices(NikoHomeControlBridgeHandler bridgeHandler, @@ -198,6 +201,18 @@ private void discoverAlarmDevices(NikoHomeControlBridgeHandler bridgeHandler, }); } + private void discoverCarChargerDevices(NikoHomeControlBridgeHandler bridgeHandler, + NikoHomeControlCommunication nhcComm) { + Map carChargerDevices = nhcComm.getCarChargerDevices(); + + carChargerDevices.forEach((deviceId, nhcCarCharger) -> { + String thingName = nhcCarCharger.getName(); + String thingLocation = nhcCarCharger.getLocation(); + addDevice(new ThingUID(THING_TYPE_CAR_CHARGER, bridgeHandler.getThing().getUID(), deviceId), + CONFIG_CAR_CHARGER_ID, deviceId, thingName, thingLocation); + }); + } + private void addDevice(ThingUID uid, String deviceIdKey, String deviceId, String thingName, @Nullable String thingLocation) { DiscoveryResultBuilder discoveryResultBuilder = DiscoveryResultBuilder.create(uid).withBridge(bridgeUID) diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlCarChargerConfig.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlCarChargerConfig.java new file mode 100644 index 0000000000000..2a5fa1791cf77 --- /dev/null +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlCarChargerConfig.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2010-2025 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.nikohomecontrol.internal.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * {@link NikoHomeControlCarChargerConfig} is the general config class for Niko Home Control Car Chargers. + * + * @author Mark Herwege - Initial Contribution + */ +@NonNullByDefault +public class NikoHomeControlCarChargerConfig { + public String carChargerId = ""; +} diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlCarChargerHandler.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlCarChargerHandler.java new file mode 100644 index 0000000000000..966243eb82cd9 --- /dev/null +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlCarChargerHandler.java @@ -0,0 +1,291 @@ +/* + * Copyright (c) 2010-2025 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.nikohomecontrol.internal.handler; + +import static org.openhab.binding.nikohomecontrol.internal.NikoHomeControlBindingConstants.*; +import static org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlConstants.*; +import static org.openhab.core.types.RefreshType.REFRESH; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.nikohomecontrol.internal.protocol.NhcCarCharger; +import org.openhab.binding.nikohomecontrol.internal.protocol.NhcCarChargerEvent; +import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlCommunication; +import org.openhab.binding.nikohomecontrol.internal.protocol.nhc2.NhcCarCharger2; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.MetricPrefix; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.library.unit.Units; +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.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link NikoHomeControlCarChargerHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Mark Herwege - Initial Contribution + */ +@NonNullByDefault +public class NikoHomeControlCarChargerHandler extends NikoHomeControlBaseHandler implements NhcCarChargerEvent { + + private final Logger logger = LoggerFactory.getLogger(NikoHomeControlCarChargerHandler.class); + + private volatile @Nullable NhcCarCharger nhcCarCharger; + + public NikoHomeControlCarChargerHandler(Thing thing) { + super(thing); + } + + @Override + void handleCommandSelection(ChannelUID channelUID, Command command) { + NhcCarCharger nhcCarCharger = this.nhcCarCharger; + if (nhcCarCharger == null) { + logger.debug("car charger device with ID {} not initialized", deviceId); + return; + } + + logger.debug("handle command {} for {}", command, channelUID); + + if (REFRESH.equals(command)) { + switch (channelUID.getId()) { + case CHANNEL_STATUS: + case CHANNEL_CHARGING_STATUS: + case CHANNEL_EV_STATUS: + case CHANNEL_COUPLING_STATUS: + case CHANNEL_POWER: + chargingStatusEvent(nhcCarCharger.getStatus(), nhcCarCharger.getChargingStatus(), + nhcCarCharger.getEvStatus(), nhcCarCharger.getCouplingStatus(), + nhcCarCharger.getElectricalPower()); + break; + case CHANNEL_CHARGING_MODE: + case CHANNEL_TARGET_DISTANCE: + case CHANNEL_TARGET_TIME: + case CHANNEL_BOOST: + case CHANNEL_REACHABLE_DISTANCE: + case CHANNEL_NEXT_CHARGING_TIME: + chargingModeEvent(nhcCarCharger.getChargingMode(), nhcCarCharger.getTargetDistance(), + nhcCarCharger.getTargetTime(), nhcCarCharger.isBoost(), + nhcCarCharger.getReachableDistance(), nhcCarCharger.getNextChargingTime()); + break; + default: + + } + } else { + switch (channelUID.getId()) { + case CHANNEL_STATUS: + if (command instanceof OnOffType onOffCommand) { + nhcCarCharger.executeCarChargerStatus(OnOffType.ON.equals(onOffCommand) ? true : false); + } + break; + case CHANNEL_CHARGING_MODE: + if (command instanceof StringType stringTypeCommand) { + String stringCommand = stringTypeCommand.toString(); + String chargingMode = CHARGINGMODES.entrySet().stream() + .filter(e -> stringCommand.equals(e.getValue())).map(e -> e.getKey()).findFirst() + .orElse(null); + if (chargingMode != null) { + nhcCarCharger.executeCarChargerChargingMode(chargingMode, nhcCarCharger.getTargetDistance(), + nhcCarCharger.getTargetTime()); + } + } + break; + case CHANNEL_TARGET_DISTANCE: + if (command instanceof QuantityType quantityCommand) { + QuantityType distance = quantityCommand.toUnit(MetricPrefix.KILO(SIUnits.METRE)); + if (distance != null) { + nhcCarCharger.executeCarChargerChargingMode(nhcCarCharger.getChargingMode(), + Math.round(distance.floatValue()), nhcCarCharger.getTargetTime()); + } + } else if (command instanceof DecimalType decimalCommand) { + BigDecimal distance = decimalCommand.toBigDecimal(); + nhcCarCharger.executeCarChargerChargingMode(nhcCarCharger.getChargingMode(), + Math.round(distance.floatValue()), nhcCarCharger.getTargetTime()); + } + break; + case CHANNEL_TARGET_TIME: + if (command instanceof DateTimeType dateTimeCommand) { + String targetTime = dateTimeCommand.format("%tR"); + nhcCarCharger.executeCarChargerChargingMode(nhcCarCharger.getChargingMode(), + nhcCarCharger.getTargetDistance(), targetTime); + } + break; + case CHANNEL_BOOST: + if (command instanceof OnOffType onOffCommand) { + nhcCarCharger.executeCarChargerChargingBoost(OnOffType.ON.equals(onOffCommand) ? true : false); + } + default: + logger.debug("unexpected command for channel {}", channelUID.getId()); + } + } + } + + @Override + public void initialize() { + initialized = false; + + NikoHomeControlCarChargerConfig config = getConfig().as(NikoHomeControlCarChargerConfig.class); + deviceId = config.carChargerId; + + NikoHomeControlBridgeHandler bridgeHandler = getBridgeHandler(); + if (bridgeHandler == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.configuration-error.invalid-bridge-handler"); + return; + } + + updateStatus(ThingStatus.UNKNOWN); + + Bridge bridge = getBridge(); + if ((bridge != null) && ThingStatus.ONLINE.equals(bridge.getStatus())) { + // We need to do this in a separate thread because we may have to wait for the + // communication to become active + commStartThread = scheduler.submit(this::startCommunication); + } + } + + @Override + synchronized void startCommunication() { + NikoHomeControlCommunication nhcComm = getCommunication(getBridgeHandler()); + + if (nhcComm == null) { + return; + } + + if (!nhcComm.communicationActive()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/offline.communication-error"); + return; + } + + NhcCarCharger nhcCarCharger = nhcComm.getCarChargerDevices().get(deviceId); + if (nhcCarCharger == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.configuration-error.deviceId"); + return; + } + + nhcCarCharger.setEventHandler(this); + + updateProperties(nhcCarCharger); + + String location = nhcCarCharger.getLocation(); + if (thing.getLocation() == null) { + thing.setLocation(location); + } + + this.nhcCarCharger = nhcCarCharger; + + initialized = true; + deviceInitialized(); + } + + @Override + void refresh() { + NhcCarCharger carCharger = nhcCarCharger; + if (carCharger != null) { + chargingModeEvent(carCharger.getChargingMode(), carCharger.getTargetDistance(), carCharger.getTargetTime(), + carCharger.isBoost(), carCharger.getReachableDistance(), carCharger.getNextChargingTime()); + chargingStatusEvent(carCharger.getStatus(), carCharger.getChargingStatus(), carCharger.getEvStatus(), + carCharger.getCouplingStatus(), carCharger.getElectricalPower()); + } + } + + @Override + public void dispose() { + NikoHomeControlCommunication nhcComm = getCommunication(getBridgeHandler()); + if (nhcComm != null) { + NhcCarCharger access = nhcComm.getCarChargerDevices().get(deviceId); + if (access != null) { + access.unsetEventHandler(); + } + } + nhcCarCharger = null; + super.dispose(); + } + + private void updateProperties(NhcCarCharger nhcCarCharger) { + Map properties = new HashMap<>(); + + if (nhcCarCharger instanceof NhcCarCharger2 access) { + properties.put(PROPERTY_DEVICE_TYPE, access.getDeviceType()); + properties.put(PROPERTY_DEVICE_TECHNOLOGY, access.getDeviceTechnology()); + properties.put(PROPERTY_DEVICE_MODEL, access.getDeviceModel()); + } + + thing.setProperties(properties); + } + + @Override + public void chargingStatusEvent(boolean status, @Nullable String chargingStatus, @Nullable String evStatus, + @Nullable String couplingStatus, @Nullable Integer electricalPower) { + NhcCarCharger nhcCarCharger = this.nhcCarCharger; + if (nhcCarCharger == null) { + logger.debug("car charger device with ID {} not initialized", deviceId); + return; + } + + updateState(CHANNEL_STATUS, OnOffType.from(status)); + if (chargingStatus != null) { + updateState(CHANNEL_CHARGING_STATUS, StringType.valueOf(CHARGINGSTATES.get(chargingStatus))); + } + if (evStatus != null) { + updateState(CHANNEL_EV_STATUS, StringType.valueOf(EVSTATES.get(evStatus))); + } + if (couplingStatus != null) { + updateState(CHANNEL_COUPLING_STATUS, StringType.valueOf(COUPLINGSTATES.get(couplingStatus))); + } + if (electricalPower != null) { + updateState(CHANNEL_POWER, new QuantityType<>(electricalPower, Units.WATT)); + } + updateStatus(ThingStatus.ONLINE); + } + + @Override + public void chargingModeEvent(@Nullable String chargingMode, float targetDistance, @Nullable String targetTime, + boolean boost, float reachableDistance, @Nullable String nextChargingTime) { + NhcCarCharger nhcCarCharger = this.nhcCarCharger; + if (nhcCarCharger == null) { + logger.debug("car charger device with ID {} not initialized", deviceId); + return; + } + + if (chargingMode != null) { + updateState(CHANNEL_CHARGING_MODE, StringType.valueOf(CHARGINGMODES.get(chargingMode))); + updateState(CHANNEL_TARGET_DISTANCE, new QuantityType<>(targetDistance, MetricPrefix.KILO(SIUnits.METRE))); + updateState(CHANNEL_REACHABLE_DISTANCE, + new QuantityType<>(reachableDistance, MetricPrefix.KILO(SIUnits.METRE))); + } + updateState(CHANNEL_BOOST, OnOffType.from(boost)); + if (targetTime != null) { + updateState(CHANNEL_TARGET_TIME, DateTimeType.valueOf(targetTime)); + } + if (nextChargingTime != null) { + updateState(CHANNEL_NEXT_CHARGING_TIME, DateTimeType.valueOf(nextChargingTime)); + } + updateStatus(ThingStatus.ONLINE); + } +} diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcCarCharger.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcCarCharger.java new file mode 100644 index 0000000000000..c2f8c6599fbfd --- /dev/null +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcCarCharger.java @@ -0,0 +1,351 @@ +/* + * Copyright (c) 2010-2025 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.nikohomecontrol.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link NhcCarCharger} class represents the charging station Niko Home Control II communication object. It + * contains all fields representing a Niko Home Control charging station and has methods to control car charging in Niko + * Home Control and receive charging station updates. The specific implementation is + * {@link org.openhab.binding.nikohomecontrol.internal.protocol.nhc2.NhcCarCharger2}. + * + * @author Mark Herwege - Initial Contribution + */ +@NonNullByDefault +public abstract class NhcCarCharger { + + private final Logger logger = LoggerFactory.getLogger(NhcCarCharger.class); + + protected NikoHomeControlCommunication nhcComm; + + protected String id; + protected String name; + protected @Nullable String location; + + protected volatile boolean status; + protected volatile @Nullable String chargingStatus; + protected volatile @Nullable String evStatus; + protected volatile @Nullable String couplingStatus; + protected volatile @Nullable Integer electricalPower; + protected volatile @Nullable String chargingMode; + protected volatile float targetDistance; + protected volatile @Nullable String targetTime; + protected volatile boolean boost; + protected volatile float reachableDistance; + protected volatile @Nullable String nextChargingTime; + + @Nullable + private NhcCarChargerEvent eventHandler; + + protected NhcCarCharger(String id, String name, @Nullable String location, NikoHomeControlCommunication nhcComm) { + this.id = id; + this.name = name; + this.location = location; + this.nhcComm = nhcComm; + } + + /** + * This method should be called when an object implementing the {@link NhcCarChargerEvent} interface is initialized. + * It keeps a record of the event handler in that object so it can be updated when the car charger receives an + * update + * from the Niko Home Control IP-interface. + * + * @param eventHandler + */ + public void setEventHandler(NhcCarChargerEvent eventHandler) { + this.eventHandler = eventHandler; + } + + /** + * This method should be called when an object implementing the {@link NhcCarChargerEvent} interface is disposed. + * It resets the reference, so no updates go to the handler anymore. + * + */ + public void unsetEventHandler() { + this.eventHandler = null; + } + + /** + * Get id of car charger. + * + * @return id + */ + public String getId() { + return id; + } + + /** + * Get name of car charger. + * + * @return car charger name + */ + public String getName() { + return name; + } + + /** + * Set name of car charger. + * + * @param name car charger name + */ + public void setName(String name) { + this.name = name; + } + + /** + * Get location name of car charger. + * + * @return location name + */ + public @Nullable String getLocation() { + return location; + } + + /** + * Set location name of car charger. + * + * @param location action location name + */ + public void setLocation(@Nullable String location) { + this.location = location; + } + + /** + * Returns the current status of the car charger. + * + * @return {@code true} if the car charger is active; {@code false} otherwise. + */ + public boolean getStatus() { + return status; + } + + /** + * Returns the current charging mode as a {@link String}. + * Possible charging modes are SOLAR, NORMAL or SMART. + * If the charging mode is not set, returns an empty string. + * + * @return the charging mode, or an empty string if not available + */ + public String getChargingMode() { + String chargingMode = this.chargingMode; + return chargingMode != null ? chargingMode : ""; + } + + /** + * Returns the extra target distance for charging in SMART mode. + * + * @return the target distance in km + */ + public float getTargetDistance() { + return targetDistance; + } + + /** + * Returns the target time for charging in SMART mode. + * If the target time is not set, returns an empty string. + * + * @return the target time, or an empty string if not set + */ + public String getTargetTime() { + String targetTime = this.targetTime; + return targetTime != null ? targetTime : ""; + } + + /** + * Checks if the car charger is currently in SMART mode boost. + * In boost mode, the car charger will not respect the current capacity limit. + * + * @return {@code true} if boost mode is active; {@code false} otherwise. + */ + public boolean isBoost() { + return boost; + } + + /** + * Returns the current charging status of the car charger. + * Possible charging status is ACTIVE, INACTIVE, BATTERY FULL or ERROR. + * + * @return the charging status, or {@code null} if the status is unavailable. + */ + public @Nullable String getChargingStatus() { + return chargingStatus; + } + + /** + * Returns the current status of the electric vehicle (EV) connected to the charger. + * Possible EV status is IDLE, CONNECTED or CHARGING. + * + * @return the EV charger status, or {@code null} if the status is unavailable. + */ + public @Nullable String getEvStatus() { + return evStatus; + } + + /** + * Returns the current coupling status of the car charger. + * Possible coupling status is OK, NO INTERNET, NO CREDENTIALS, INVALID CREDENTIALS, + * CONNECTION ERROR, CONNECTION TIMEOUT, API ERROR or UNKNOWN ERROR. + * + * @return the coupling status, or {@code null} if the status is unavailable. + */ + public @Nullable String getCouplingStatus() { + return couplingStatus; + } + + /** + * Returns the reachable distance when charging in SMART mode. + * + * @return the reachable distance in km + */ + public float getReachableDistance() { + return reachableDistance; + } + + /** + * Returns the next scheduled charging time. + * If the next charging time is not set, returns an empty string. + * + * @return the next charging time, or an empty string if not available + */ + public String getNextChargingTime() { + String nextChargingTime = this.nextChargingTime; + return nextChargingTime != null ? nextChargingTime : ""; + } + + /** + * Returns the current charging electrical power value. + * If the value is not set (i.e., {@code electricalPower} is {@code null}), + * this method returns {@code 0}. + * + * @return the electrical power value, or {@code 0} if not available + */ + public int getElectricalPower() { + Integer electricalPower = this.electricalPower; + return electricalPower != null ? electricalPower : 0; + } + + /** + * Updates the status and related channels of the car charger thing through the event handler. + * If a parameter is {@code null}, the corresponding channel remains unchanged. + * + * @param status the new status of the car charger (true if active, false otherwise) + * @param chargingStatus the new charging status, or {@code null} to keep the current value + * @param evStatus the new EV status, or {@code null} to keep the current value + * @param couplingStatus the new coupling status, or {@code null} to keep the current value + * @param electricalPower the new electrical power value, or {@code null} to keep the current value + */ + public void setStatus(boolean status, @Nullable String chargingStatus, @Nullable String evStatus, + @Nullable String couplingStatus, @Nullable Integer electricalPower) { + this.status = status; + this.chargingStatus = chargingStatus != null ? chargingStatus : this.chargingStatus; + this.evStatus = evStatus != null ? evStatus : this.evStatus; + this.couplingStatus = couplingStatus != null ? couplingStatus : this.couplingStatus; + this.electricalPower = electricalPower != null ? electricalPower : this.electricalPower; + + NhcCarChargerEvent eventHandler = this.eventHandler; + if (eventHandler != null) { + logger.debug( + "update charger status for {} with status {}, charging status {}, EV status {}, coupling status {}, power {}", + id, this.status, this.chargingStatus, this.evStatus, this.couplingStatus, this.electricalPower); + eventHandler.chargingStatusEvent(this.status, this.chargingStatus, this.evStatus, this.couplingStatus, + this.electricalPower); + } + } + + /** + * Updates the charging mode and related channels for the car charger thing. + * If a parameter is {@code null}, the existing value is retained. + * After updating, notifies the event handler (if present) with the new states. + * + * @param chargingMode the desired charging mode, or {@code null} to keep the current mode + * @param targetDistance the target distance to charge for, or {@code null} to keep the current value + * @param targetTime the target time for charging, or {@code null} or empty to keep the current value + * @param boost whether boost mode is enabled, or {@code null} to keep the current value + * @param reachableDistance the currently reachable distance, or {@code null} to keep the current value + * @param nextChargingTime the next scheduled charging time, or {@code null} or empty to keep the current value + */ + public void setChargingMode(@Nullable String chargingMode, @Nullable Float targetDistance, + @Nullable String targetTime, @Nullable Boolean boost, @Nullable Float reachableDistance, + @Nullable String nextChargingTime) { + this.chargingMode = chargingMode != null ? chargingMode : this.chargingMode; + this.targetDistance = targetDistance != null ? targetDistance : this.targetDistance; + this.targetTime = targetTime != null && !targetTime.isEmpty() ? targetTime : this.targetTime; + this.boost = boost != null ? boost : this.boost; + this.reachableDistance = reachableDistance != null ? reachableDistance : this.reachableDistance; + this.nextChargingTime = nextChargingTime != null && !nextChargingTime.isEmpty() ? nextChargingTime + : this.nextChargingTime; + NhcCarChargerEvent eventHandler = this.eventHandler; + if (eventHandler != null) { + logger.debug( + "update charging mode channel states for {} with charging mode {}, target distance {}, target time {}, boost {}, reachable distance {}, next charging time {}", + id, this.chargingMode, this.targetDistance, this.targetTime, this.boost, this.reachableDistance, + this.nextChargingTime); + eventHandler.chargingModeEvent(this.chargingMode, this.targetDistance, this.targetTime, this.boost, + this.reachableDistance, this.nextChargingTime); + } + } + + /** + * Method called when car charger is removed from the Niko Home Control Controller. + */ + public void carChargerDeviceRemoved() { + logger.debug("car charger removed {}, {}", id, name); + NhcCarChargerEvent eventHandler = this.eventHandler; + if (eventHandler != null) { + eventHandler.deviceRemoved(); + unsetEventHandler(); + } + } + + /** + * Changes the status of the car charger in the Niko Home Control system. + *

+ * This method sends a command to update the car charger status for the specified device ID. + * + * @param status {@code true} to turn the car charger on, {@code false} to turn it off. + */ + public void executeCarChargerStatus(boolean status) { + logger.debug("change car charger status for {} to {}", id, status); + nhcComm.executeCarChargerStatus(id, status); + } + + /** + * Changes the charging mode of the car charger in the Niko Home Control system. + * + * @param chargingMode The desired charging mode to set (SOLAR, NORMAL or SMART). + * @param targetDistance The target distance (in kilometers) to be achieved during charging for SMART mode.. + * @param targetTime The target time (in ISO 8601 format or HH:mm) by which charging should be completed for SMART + * mode. + */ + public void executeCarChargerChargingMode(String chargingMode, float targetDistance, String targetTime) { + logger.debug("change car charger charging mode for {} to {}, target {} at {}", id, chargingMode, targetDistance, + targetTime); + nhcComm.executeCarChargerChargingMode(id, chargingMode, targetDistance, targetTime); + } + + /** + * Executes a command to change the charging boost state for SMART charging of the car charger in the Niko Home + * Control system. + * When boost is true, the capacity limit may not be respected. + * + * @param boost true to enable charging boost, false to disable it + */ + public void executeCarChargerChargingBoost(boolean boost) { + logger.debug("change car charger boost for {} to {}", id, boost); + nhcComm.executeCarChargerChargingBoost(id, boost); + } +} diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcCarChargerEvent.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcCarChargerEvent.java new file mode 100644 index 0000000000000..a7908e7bad59d --- /dev/null +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcCarChargerEvent.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2010-2025 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.nikohomecontrol.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link NhcCarChargerEvent} interface is used to pass car charger events received from the Niko Home Control + * controller to the consuming client. It is designed to pass events to openHAB handlers that implement this interface. + * Because of the design, the org.openhab.binding.nikohomecontrol.internal.protocol package can be extracted and used + * independent of openHAB. + * + * @author Mark Herwege - Initial Contribution + */ +@NonNullByDefault +public interface NhcCarChargerEvent extends NhcBaseEvent { + + /** + * Handles an update event related to the charging status of a car charger. + * This event will update the respective car charger thing channels. + * + * @param status true if charging is active, false otherwise. + * @param chargingStatus the current charging status as a string, or null if unavailable. + * @param evStatus the electric vehicle status as a string, or null if unavailable. + * @param couplingStatus the coupling status as a string, or null if unavailable. + * @param electricalPower the electrical power being delivered (in watts), or null if unavailable. + */ + void chargingStatusEvent(boolean status, @Nullable String chargingStatus, @Nullable String evStatus, + @Nullable String couplingStatus, @Nullable Integer electricalPower); + + /** + * Handles an update event related to the car charger's charging mode. + * This event will update the respective car charger thing channels. + * + * @param chargingMode the current charging mode, or {@code null} if not available + * @param targetDistance the target distance to be achieved by charging (in kilometers) + * @param targetTime the target time to reach the desired charge, or {@code null} if not specified + * @param boost {@code true} if boost mode is enabled; {@code false} otherwise + * @param reachableDistance the currently reachable distance with the SMART charging mode (in kilometers) + * @param nextChargingTime the next scheduled charging time, or {@code null} if not set + */ + void chargingModeEvent(@Nullable String chargingMode, float targetDistance, @Nullable String targetTime, + boolean boost, float reachableDistance, @Nullable String nextChargingTime); +} diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NikoHomeControlCommunication.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NikoHomeControlCommunication.java index 0f227a3a56d46..9ab87b9d1313e 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NikoHomeControlCommunication.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NikoHomeControlCommunication.java @@ -38,6 +38,7 @@ * * * @author Mark Herwege - Initial Contribution + * @author Mark Herwege - Add car chargers */ @NonNullByDefault public abstract class NikoHomeControlCommunication { @@ -50,6 +51,7 @@ public abstract class NikoHomeControlCommunication { protected final Map accessDevices = new ConcurrentHashMap<>(); protected final Map videoDevices = new ConcurrentHashMap<>(); protected final Map alarmDevices = new ConcurrentHashMap<>(); + protected final Map carChargerDevices = new ConcurrentHashMap<>(); protected final NhcControllerEvent handler; @@ -202,6 +204,15 @@ public Map getAlarmDevices() { return alarmDevices; } + /** + * Return all car chargers devices in the Niko Home Control Controller. + * + * @return Map<String, {@link NhcCarCharger}> + */ + public Map getCarChargerDevices() { + return carChargerDevices; + } + /** * Execute an action command by sending it to Niko Home Control. * @@ -353,4 +364,34 @@ public void executeArm(String alarmId) { */ public void executeDisarm(String alarmId) { } + + /** + * Sends a command to set the status of a car charger to Niko Home Control. + * + * @param carChargerId the unique identifier of the car charger + * @param status the desired status of the car charger (true to enable, false to disable) + */ + public void executeCarChargerStatus(String carChargerId, boolean status) { + } + + /** + * Executes a command to set the charging mode for a car charger to Niko Home Control. + * + * @param carChargerId the unique identifier of the car charger device + * @param chargingMode the desired charging mode (SOLAR, NORMAL or SMART) + * @param targetDistance the target distance (in kilometers) to be achieved by charging + * @param targetTime the target time (in ISO 8601 format or HH:mm) by which charging should be completed + */ + public void executeCarChargerChargingMode(String carChargerId, String chargingMode, float targetDistance, + String targetTime) { + } + + /** + * Executes a charging boost command for the specified car charger. + * + * @param carChargerId the unique identifier of the car charger to control + * @param boost {@code true} to enable charging boost, {@code false} to disable it + */ + public void executeCarChargerChargingBoost(String carChargerId, boolean boost) { + } } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NikoHomeControlConstants.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NikoHomeControlConstants.java index b038cd86ee78f..d8fce8b415334 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NikoHomeControlConstants.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NikoHomeControlConstants.java @@ -86,4 +86,40 @@ public static enum MeterType { public static final String NHCALARM = "Alarm"; public static final Map ALARMSTATES = Map.of(NHCOFF, "DISARMED", NHCPREARMED, "PREARMED", NHCDETECTORPROBLEM, "DETECTOR PROBLEM", NHCARMED, "ARMED", NHCPREALARM, "PREALARM", NHCALARM, "ALARM"); + + // NhcII car charger charging modes + public static final String NHCSOLAR = "Solar"; + public static final String NHCNORMAL = "Normal"; + public static final String NHCSMART = "Smart"; + public static final Map CHARGINGMODES = Map.of(NHCSOLAR, "SOLAR", NHCNORMAL, "NORMAL", NHCSMART, + "SMART"); + + // NhcII car charger charging status + // NHCACTIVE = "Active" already defined + public static final String NHCINACTIVE = "Inactive"; + public static final String NHCBATTERYFULL = "BatteryFull"; + public static final String NHCERROR = "Error"; + public static final Map CHARGINGSTATES = Map.of(NHCACTIVE, "ACTIVE", NHCINACTIVE, "INACTIVE", + NHCBATTERYFULL, "BATTERY FULL", NHCERROR, "ERROR"); + + // NhcII car charger EV status + // NHCIDLE = "Idle" already defined + public static final String NHCCONNECTED = "Connected"; + public static final String NHCCHARGING = "Charging"; + public static final Map EVSTATES = Map.of(NHCIDLE, "IDLE", NHCCONNECTED, "CONNECTED", NHCCHARGING, + "CHARGING"); + + // NhcII car charger coupling status + public static final String NHCOK = "Ok"; + public static final String NHCNOINTERNET = "NoInternet"; + public static final String NHCNOCREDENTIALS = "NoCredentials"; + public static final String NHCINVALIDCREDENTIALS = "InvalidCredentials"; + public static final String NHCCONNECTIONERROR = "ConnectionError"; + public static final String NHCCONNECTIONTIMEOUT = "ConnectionTimeout"; + public static final String NHCAPIERROR = "ApiError"; + public static final String NHCUNKNOWNERROR = "UnknownError"; + public static final Map COUPLINGSTATES = Map.of(NHCOK, "OK", NHCNOINTERNET, "NO INTERNET", + NHCNOCREDENTIALS, "NO CREDENTIALS", NHCINVALIDCREDENTIALS, "INVALID CREDENTIALS", NHCCONNECTIONERROR, + "CONNECTION ERROR", NHCCONNECTIONTIMEOUT, "CONNECTION TIMEOUT", NHCAPIERROR, "API ERROR", NHCUNKNOWNERROR, + "UNKNOWN ERROR"); } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcCarCharger2.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcCarCharger2.java new file mode 100644 index 0000000000000..efc872045f7e2 --- /dev/null +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcCarCharger2.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2010-2025 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.nikohomecontrol.internal.protocol.nhc2; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.nikohomecontrol.internal.protocol.NhcCarCharger; +import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlCommunication; + +/** + * The {@link NhcCarCharger2} class represents the charging station Niko Home Control II communication object. It + * contains all fields representing a Niko Home Control charging station and has methods to control car charging in Niko + * Home Control and receive charging station updates. + * + * @author Mark Herwege - Initial Contribution + */ +@NonNullByDefault +public class NhcCarCharger2 extends NhcCarCharger { + + private final String deviceType; + private final String deviceTechnology; + private final String deviceModel; + + NhcCarCharger2(String id, String name, String deviceType, String deviceTechnology, String deviceModel, + @Nullable String location, NikoHomeControlCommunication nhcComm) { + super(id, name, location, nhcComm); + this.deviceType = deviceType; + this.deviceTechnology = deviceTechnology; + this.deviceModel = deviceModel; + } + + /** + * @return type as returned from Niko Home Control + */ + public String getDeviceType() { + return deviceType; + } + + /** + * @return technology as returned from Niko Home Control + */ + public String getDeviceTechnology() { + return deviceTechnology; + } + + /** + * @return model as returned from Niko Home Control + */ + public String getDeviceModel() { + return deviceModel; + } +} diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcDevice2.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcDevice2.java index e75c24454b817..d14de570bb85d 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcDevice2.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcDevice2.java @@ -22,6 +22,7 @@ * when creating the state update json to send to the Connected Controller. * * @author Mark Herwege - Initial Contribution + * @author Mark Herwege - Add car chargers */ @NonNullByDefault class NhcDevice2 { @@ -35,6 +36,7 @@ static class NhcProperty { String aligned; @Nullable String basicState; + // fields for motors @Nullable String action; @@ -42,6 +44,7 @@ static class NhcProperty { String position; @Nullable String moving; + // fields for thermostats and hvac @Nullable String setpointTemperature; @@ -67,9 +70,11 @@ static class NhcProperty { String thermostatOn; @Nullable String hvacOn; + // fields for fans and ventilation @Nullable String fanSpeed; + // fields for electricity metering @Nullable String electricalEnergy; @@ -89,9 +94,11 @@ static class NhcProperty { String electricalPowerProductionThresholdExceeded; @Nullable String reportInstantUsage; + // fields for access control @Nullable String doorlock; + // fields for video devices @Nullable String ipAddress; @@ -103,6 +110,7 @@ static class NhcProperty { String callStatus03; @Nullable String callStatus04; + // fields for alarms @Nullable String internalState; @@ -112,16 +120,41 @@ static class NhcProperty { String alarmTriggered; @Nullable String control; + + // fields for car chargers + @Nullable + String chargingStatus; + @Nullable + String evStatus; + @Nullable + String couplingStatus; + @Nullable + String chargingMode; + @Nullable + String targetDistance; + @Nullable + String targetTime; + @Nullable + String boost; + @Nullable + String reachableDistance; + @Nullable + String nextChargingTime; } static class NhcTrait { @Nullable String macAddress; + // fields for metering @Nullable String channel; @Nullable String meterType; + + // fields for car chargers + @Nullable + String playerName; } static class NhcParameter { @@ -131,6 +164,7 @@ static class NhcParameter { String locationName; @Nullable String locationIcon; + // fields for electricity metering @Nullable String flow; @@ -140,6 +174,7 @@ static class NhcParameter { String clampType; @Nullable String shortName; + // fields for access control @Nullable String buttonId; @@ -149,6 +184,7 @@ static class NhcParameter { String declineCallAppliedOnAllDevices; @Nullable String iconCode; + // fields for video devices @Nullable String mjpegUri; diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NikoHomeControlCommunication2.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NikoHomeControlCommunication2.java index 67db2d3cb3462..d1a5fcba096ff 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NikoHomeControlCommunication2.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NikoHomeControlCommunication2.java @@ -36,6 +36,7 @@ import org.openhab.binding.nikohomecontrol.internal.protocol.NhcAccess; import org.openhab.binding.nikohomecontrol.internal.protocol.NhcAction; import org.openhab.binding.nikohomecontrol.internal.protocol.NhcAlarm; +import org.openhab.binding.nikohomecontrol.internal.protocol.NhcCarCharger; import org.openhab.binding.nikohomecontrol.internal.protocol.NhcControllerEvent; import org.openhab.binding.nikohomecontrol.internal.protocol.NhcMeter; import org.openhab.binding.nikohomecontrol.internal.protocol.NhcThermostat; @@ -72,6 +73,7 @@ * * * @author Mark Herwege - Initial Contribution + * @author Mark Herwege - Add car chargers */ @NonNullByDefault public class NikoHomeControlCommunication2 extends NikoHomeControlCommunication @@ -394,6 +396,8 @@ private void addDevice(NhcDevice2 device) { addThermostatDevice(device, location); } else if ("centralmeter".equals(device.type) || "energyhome".equals(device.type)) { addMeterDevice(device, location); + } else if ("chargingstation".equals(device.type)) { + addCarChargerDevice(device, location); } else { logger.debug("device type {} and model {} not supported for {}, {}", device.type, device.model, device.uuid, device.name); @@ -587,6 +591,19 @@ private void addAlarmDevice(NhcDevice2 device, @Nullable String location) { alarmDevices.put(device.uuid, nhcAlarm); } + private void addCarChargerDevice(NhcDevice2 device, @Nullable String location) { + NhcCarCharger nhcCarCharger = carChargerDevices.get(device.uuid); + if (nhcCarCharger != null) { + nhcCarCharger.setName(device.name); + nhcCarCharger.setLocation(location); + } else { + logger.debug("adding car charger device {} model {}, {}", device.uuid, device.model, device.name); + nhcCarCharger = new NhcCarCharger2(device.uuid, device.name, device.type, device.technology, device.model, + location, this); + } + carChargerDevices.put(device.uuid, nhcCarCharger); + } + private void removeDevice(NhcDevice2 device) { NhcAction action = actions.get(device.uuid); NhcThermostat thermostat = thermostats.get(device.uuid); @@ -594,6 +611,7 @@ private void removeDevice(NhcDevice2 device) { NhcAccess access = accessDevices.get(device.uuid); NhcVideo video = videoDevices.get(device.uuid); NhcAlarm alarm = alarmDevices.get(device.uuid); + NhcCarCharger carCharger = carChargerDevices.get(device.uuid); if (action != null) { action.actionRemoved(); actions.remove(device.uuid); @@ -612,6 +630,9 @@ private void removeDevice(NhcDevice2 device) { } else if (alarm != null) { alarm.alarmDeviceRemoved(); alarmDevices.remove(device.uuid); + } else if (carCharger != null) { + carCharger.carChargerDeviceRemoved(); + carChargerDevices.remove(device.uuid); } } @@ -628,6 +649,7 @@ private void updateState(NhcDevice2 device) { NhcAccess accessDevice = accessDevices.get(device.uuid); NhcVideo videoDevice = videoDevices.get(device.uuid); NhcAlarm alarm = alarmDevices.get(device.uuid); + NhcCarCharger carCharger = carChargerDevices.get(device.uuid); if (action != null) { updateActionState((NhcAction2) action, deviceProperties); @@ -641,6 +663,8 @@ private void updateState(NhcDevice2 device) { updateVideoState((NhcVideo2) videoDevice, deviceProperties); } else if (alarm != null) { updateAlarmState((NhcAlarm2) alarm, deviceProperties); + } else if (carCharger != null) { + updateCarChargerState((NhcCarCharger2) carCharger, deviceProperties); } else { logger.trace("No known device for {}", device.uuid); } @@ -852,6 +876,45 @@ private void updateAlarmState(NhcAlarm2 alarmDevice, List devicePro } } + private void updateCarChargerState(NhcCarCharger2 carChargerDevice, List deviceProperties) { + String status = deviceProperties.stream().map(p -> p.status).filter(Objects::nonNull).findFirst().orElse(null); + String chargingStatus = deviceProperties.stream().map(p -> p.chargingStatus).filter(Objects::nonNull) + .findFirst().orElse(null); + String evStatus = deviceProperties.stream().map(p -> p.evStatus).filter(Objects::nonNull).findFirst() + .orElse(null); + String couplingStatus = deviceProperties.stream().map(p -> p.couplingStatus).filter(Objects::nonNull) + .findFirst().orElse(null); + Integer electricalPower = deviceProperties.stream().map(p -> p.electricalPower) + .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null) + .filter(Objects::nonNull).findFirst().orElse(null); + if (status != null || chargingStatus != null || evStatus != null || couplingStatus != null + || electricalPower != null) { + logger.debug( + "setting car charger device {} status to {}, charging status to {}, EV status to {}, coupling status to {}, electrical power to {}", + carChargerDevice.getId(), status, chargingStatus, evStatus, couplingStatus, electricalPower); + carChargerDevice.setStatus(NHCON.equals(status) ? true : false, chargingStatus, evStatus, couplingStatus, + electricalPower); + } + + String chargingMode = deviceProperties.stream().map(p -> p.chargingMode).filter(Objects::nonNull).findFirst() + .orElse(null); + Float targetDistance = deviceProperties.stream().map(p -> p.targetDistance).filter(Objects::nonNull).findFirst() + .map(Float::parseFloat).orElse(null); + String targetTime = deviceProperties.stream().map(p -> p.targetTime).filter(Objects::nonNull).findFirst() + .orElse(null); + Boolean boost = deviceProperties.stream().map(p -> p.boost).filter(Objects::nonNull).findFirst() + .map(b -> NHCTRUE.equals(b) ? true : false).orElse(null); + Float reachableDistance = deviceProperties.stream().map(p -> p.reachableDistance).filter(Objects::nonNull) + .findFirst().map(Float::parseFloat).orElse(null); + String nextChargingTime = deviceProperties.stream().map(p -> p.nextChargingTime).filter(Objects::nonNull) + .findFirst().orElse(null); + if (chargingMode != null || targetDistance != null || targetTime != null || boost != null + || reachableDistance != null || nextChargingTime != null) { + carChargerDevice.setChargingMode(chargingMode, targetDistance, targetTime, boost, reachableDistance, + nextChargingTime); + } + } + @Override public void executeAction(String actionId, String value) { NhcMessage2 message = new NhcMessage2(); @@ -1198,6 +1261,104 @@ private void executeAlarm(String alarmId, String state) { sendDeviceMessage(topic, gsonMessage); } + @Override + public void executeCarChargerStatus(String carChargerId, boolean status) { + NhcMessage2 message = new NhcMessage2(); + + message.method = "devices.control"; + List params = new ArrayList<>(); + NhcMessageParam param = new NhcMessageParam(); + params.add(param); + message.params = params; + List devices = new ArrayList<>(); + NhcDevice2 device = new NhcDevice2(); + devices.add(device); + param.devices = devices; + device.uuid = carChargerId; + List deviceProperties = new ArrayList<>(); + NhcProperty property = new NhcProperty(); + deviceProperties.add(property); + device.properties = deviceProperties; + + NhcCarCharger2 carChargerDevice = (NhcCarCharger2) carChargerDevices.get(carChargerId); + if (carChargerDevice == null) { + return; + } + + property.status = status ? NHCON : NHCOFF; + + String topic = profile + "/control/devices/cmd"; + String gsonMessage = gson.toJson(message); + sendDeviceMessage(topic, gsonMessage); + } + + @Override + public void executeCarChargerChargingMode(String carChargerId, String chargingMode, float targetDistance, + String targetTime) { + NhcMessage2 message = new NhcMessage2(); + + message.method = "devices.control"; + List params = new ArrayList<>(); + NhcMessageParam param = new NhcMessageParam(); + params.add(param); + message.params = params; + List devices = new ArrayList<>(); + NhcDevice2 device = new NhcDevice2(); + devices.add(device); + param.devices = devices; + device.uuid = carChargerId; + List deviceProperties = new ArrayList<>(); + NhcProperty property = new NhcProperty(); + deviceProperties.add(property); + device.properties = deviceProperties; + + NhcCarCharger2 carChargerDevice = (NhcCarCharger2) carChargerDevices.get(carChargerId); + if (carChargerDevice == null) { + return; + } + + property.chargingMode = chargingMode; + if (NHCSOLAR.equals(chargingMode)) { + property.targetDistance = String.valueOf(targetDistance); + property.targetTime = targetTime; + } + + String topic = profile + "/control/devices/cmd"; + String gsonMessage = gson.toJson(message); + sendDeviceMessage(topic, gsonMessage); + } + + @Override + public void executeCarChargerChargingBoost(String carChargerId, boolean boost) { + NhcMessage2 message = new NhcMessage2(); + + message.method = "devices.control"; + List params = new ArrayList<>(); + NhcMessageParam param = new NhcMessageParam(); + params.add(param); + message.params = params; + List devices = new ArrayList<>(); + NhcDevice2 device = new NhcDevice2(); + devices.add(device); + param.devices = devices; + device.uuid = carChargerId; + List deviceProperties = new ArrayList<>(); + NhcProperty property = new NhcProperty(); + deviceProperties.add(property); + device.properties = deviceProperties; + + NhcCarCharger2 carChargerDevice = (NhcCarCharger2) carChargerDevices.get(carChargerId); + if (carChargerDevice == null) { + return; + } + + property.boost = boost ? NHCTRUE : NHCFALSE; + + String topic = profile + "/control/devices/cmd"; + String gsonMessage = gson.toJson(message); + sendDeviceMessage(topic, gsonMessage); + } + private void sendDeviceMessage(String topic, String gsonMessage) { try { mqttConnection.connectionPublish(topic, gsonMessage); diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/i18n/nikohomecontrol.properties b/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/i18n/nikohomecontrol.properties index e3112621deae7..9ab495936cbf9 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/i18n/nikohomecontrol.properties +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/i18n/nikohomecontrol.properties @@ -64,6 +64,9 @@ accessRingAndComeInDescription = Access Control with ring and come in function i alarmLabel = Alarm alarmDescription = Alarm system in the Niko Home Control system +carChargerLabel = Car Charger +carChargerDescription = Car charger in the Niko Home Control system + deviceConfigDeviceIdLabel = Device ID deviceConfigDeviceIdDescription = Niko Home Control device ID @@ -163,6 +166,44 @@ channelOptionAlarmStateArmed = Armed channelOptionAlarmStatePreAlarm = Pre-alarm channelOptionAlarmStateAlarm = Alarm +channelChargingStatusLabel = Car Charging Status +channelChargingStatusDescription = Car charger charging status +channelOptionChargingStatusActive = Active +channelOptionChargingStatusInactive = Inactive +channelOptionChargingStatusBatteryFull = Battery Full +channelChargingStatusError = Error +channelEvStatusLabel = EV Status +channelEvStatusDescription = EV car status +channelOptionEvStatusIdle = Idle +channelOptionEvStatusConnected = Connected +channelOptionEvStatusCharging = Charging +channelCouplingStatusLabel = Coupling Status +channelCouplingStatusDescription = Car charger coupling status +channelOptionCouplingStatusOk = OK +channelOptionCouplingStatusNoInternet = No Internet +channelOptionCouplingStatusNoCredentials = No Credentials +channelOptionCouplingStatusInvalidCredentials = Invalid Credentials +channelOptionCouplingStatusConnectionError = Connection Error +channelOptionCouplingStatusConnectionTimeout = Connection Timeout +channelOptionCouplingStatusApiError = API Error +channelOptionCouplingStatusUnknownError = Unknown Error + +channelChargingModeLabel = Charging Mode +channelChargingModeDescription = Car charging mode +channelOptionChargingModeSolar = Solar +channelOptionChargingModeNormal = Normal +channelOptionChargingModeSmart = Smart +channelTargetDistanceLabel = Target Distance +channelTargetDistanceDescription = Target distance for smart mode charging +channelTargetTimeLabel = Target Time +channelTargetTimeDescription = Target time for smart mode charging +channelBoostLabel = Boost +channelBoostDescription = Boost charging for normal mode charging over current capacity tariff limit +channelReachableDistanceLabel = Reachable Distance +channelReachableDistanceDescription = Reachable distance for smart mode charging at set target time +channelNextChargingTimeLabel = Next Charging Time +channelNextChargingTimeDescription = Next charging start time + channelAlarmLabel = Alarm channelAlarmDescription = Alarm from Niko Home Control diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml index 3c35a1a3ca6dd..3e115a941cda5 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml @@ -369,6 +369,33 @@ + + + + + + @text/carChargerDescription + EVSE + + + + + + + + + + + + + + + + + @text/deviceConfigDeviceIdDescription + + + Switch @@ -624,6 +651,102 @@ + + String + + @text/channelChargingStatusDescription + + Status + + + + + + + + + + + + String + + @text/channelEvStatusDescription + + Status + + + + + + + + + + + String + + @text/channelCouplingStatusDescription + + Status + + + + + + + + + + + + + + + + String + + @text/channelChargingModeDescription + + Control + + + + + + + + + + + Number:Length + + @text/channelTargetDistanceDescription + + + + DateTime + + @text/channelTargetTimeDescription + + + + Switch + + @text/channelBoostDescription + + + Number:Length + + @text/channelReachableDistanceDescription + + + + DateTime + + @text/channelNextChargingTimeDescription + + + trigger From 97ca57f4a4f38617cb24e6db446235d43ec98db5 Mon Sep 17 00:00:00 2001 From: Mark Herwege Date: Wed, 24 Sep 2025 19:37:46 +0200 Subject: [PATCH 02/13] grid power measurements Signed-off-by: Mark Herwege --- .../README.md | 81 ++++++++++--------- .../NikoHomeControlBindingConstants.java | 3 + .../handler/NikoHomeControlMeterHandler.java | 35 +++++++- .../internal/protocol/NhcMeter.java | 66 ++++++++++++++- .../internal/protocol/NhcMeterEvent.java | 21 +++++ .../internal/protocol/nhc2/NhcDevice2.java | 2 + .../nhc2/NikoHomeControlCommunication2.java | 24 ++++-- .../OH-INF/i18n/nikohomecontrol.properties | 6 ++ .../resources/OH-INF/thing/thing-types.xml | 22 +++++ 9 files changed, 211 insertions(+), 49 deletions(-) diff --git a/bundles/org.openhab.binding.nikohomecontrol/README.md b/bundles/org.openhab.binding.nikohomecontrol/README.md index 948b851049311..b3c9950358074 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/README.md +++ b/bundles/org.openhab.binding.nikohomecontrol/README.md @@ -194,46 +194,49 @@ Thing nikohomecontrol:carCharger:mybridge:mycarcharger [ carChargerId="abcdef01- ## Channels -| Channel Type ID | RW | Advanced | Item Type | Thing Types | Description | -|-----------------|:--:|:--------:|--------------------|-------------|-----------------------------------------------------------------------------------------------------| -| button | RW | | Switch | pushButton | stateless action control, `autoupdate="false"` by default. Only accepts `ON` | -| switch | RW | | Switch | onOff | on/off switches with state control, such as light switches | -| brightness | RW | | Dimmer | dimmer | control dimmer light intensity. OnOff, IncreaseDecrease and Percent command types are supported. Note that sending an `ON` command will switch the dimmer to the value stored when last turning the dimmer off, or 100% depending on the configuration in the Niko Home Control Controller. This can be changed with the Niko Home Control programming software | -| rollershutter | RW | | Rollershutter | blind | control rollershutter or blind. UpDown, StopMove and Percent command types are supported | -| measured | R | | Number:Temperature | thermostat | current temperature. Because of API restrictions, NHC II will only report in 0.5°C increments | -| heatingmode | RW | | String | thermostat | current thermostat mode. Allowed values are Day, Night, Eco, Off, Cool, Prog1, Prog2, Prog3. Setting `heatingmode` will reset the `setpoint` channel to the standard value for the mode in the controller | -| mode | RW | X | Number | thermostat | current thermostat mode, same meaning as `heatingmode`, but numeric values (0=Day, 1=Night, 2=Eco, 3=Off, 4=Cool, 5=Prog1, 6=Prog2, 7=Prog3). Setting `mode` will reset the `setpoint` channel to the standard value for the mode in the controller. This channel is kept for binding backward compatibility | -| setpoint | RW | | Number:Temperature | thermostat | current thermostat setpoint. Updating the `setpoint` will overrule the temperature setpoint defined by `heatingmode` or `mode` for `overruletime` duration. Because of API restrictions, NHC II will only report in 0.5°C increments | -| overruletime | RW | | Number | thermostat | used to set the total duration in minutes to apply the setpoint temperature set in the `setpoint` channel before the thermostat returns to the setting from its mode | -| heatingdemand | R | | String | thermostat | indicating if the system is actively heating/cooling. This channel will have value Heating, Cooling or None. For NHC I this is set by the binding from the temperature difference between `setpoint` and `measured`. It therefore may incorrectly indicate cooling even when the system does not have active cooling capabilities | -| demand | R | X | Number | thermostat | indicating if the system is actively heating/cooling, same as `heatingdemand` but numeric values (-1=Cooling, 0=None, 1=Heating) | -| power | R | | Number:Power | energyMeterLive | instant power consumption/production (negative for production), refreshed every 2s. Linking this channel starts an intensive communication flow with the controller and should only be done when appropriate | -| energy | R | | Number:Energy | energyMeterLive, energyMeter | total energy meter reading | -| energyday | R | | Number:Energy | energyMeterLive, energyMeter | day energy meter reading | -| gas | R | | Number:Volume | gasMeter | total gas meter reading | -| gasday | R | | Number:Volume | gasMeter | day gas meter reading | -| water | R | | Number:Volume | waterMeter | total water meter reading | -| waterday | R | | Number:Volume | waterMeter | day water meter reading | -| measurementtime | R | | DateTimeType | energyMeterLive, energyMeter, gasMeter, waterMeter | last meter reading time | -| bellbutton | RW | | Switch | access, accessRingAndComeIn | bell button connected to access device, including buttons on video phone devices linked to an access device. The bell can also be triggered by an `ON` command, `autoupdate="false"` by default | -| ringandcomein | RW | | Switch | accessRingAndComeIn | provide state and turn automatic door unlocking at bell ring on/off | -| lock | RW | | Switch | access, accessRingAndComeIn | provide doorlock state and unlock the door by sending an `OFF` command. `autoupdate="false"` by default | -| arm | RW | | Switch | alarm | arm/disarm alarm, will change state (on/off) immediately. Note some integrations (Homekit, Google Home, ...) may require String states for an alarm system (ARMED/DISARMED). This can be achieved using an extra item and a rule updated by/commanding an item linked to this channel | -| armed | RW | | Switch | alarm | state of the alarm system (on/off), will only turn on after pre-armed period when arming | -| state | R | | String | alarm | state of the alarm system (DISARMED, PREARMED, ARMED, PREALARM, ALARM, DETECTOR PROBLEM) | -| status | RW | | Switch | carCharger | status of the car charger (on/off) | -| chargingStatus | R | | String | carCharger | charging status of the car charger (ACTIVE, INACTIVE, BATTERY FULL or ERROR) | -| evStatus | R | | String | carCharger | status of the electric vehicle (IDLE, CONNECTED or CHARGING) | -| couplingStatus | R | | String | carCharger | coupling status (OK, NO INTERNET, NO CREDENTIALS, INVALID CREDENTIALS, CONNECTION ERROR, CONNECTION TIMEOUT, API ERROR or UNKNOWN ERROR) | -| electricalPower | R | | Number:Power | carCharger | current charging power | -| chargingMode | RW | | String | carCharger | charging mode (SOLAR, NORMAL or SMART) | -| targetDistance | RW | | Number:Length | carCharger | target distance to achieve in charging activity | -| targetTime | RW | | DateTime | carCharger | time by which the target distance should be achieved | -| boost | RW | | Switch | carCharger | boost charging to maximum achievable, not respecting capacity limit | +| Channel Type ID | RW | Advanced | Item Type | Thing Types | Description | +|-------------------|:--:|:--------:|--------------------|-------------|-----------------------------------------------------------------------------------------------------| +| button | RW | | Switch | pushButton | stateless action control, `autoupdate="false"` by default. Only accepts `ON` | +| switch | RW | | Switch | onOff | on/off switches with state control, such as light switches | +| brightness | RW | | Dimmer | dimmer | control dimmer light intensity. OnOff, IncreaseDecrease and Percent command types are supported. Note that sending an `ON` command will switch the dimmer to the value stored when last turning the dimmer off, or 100% depending on the configuration in the Niko Home Control Controller. This can be changed with the Niko Home Control programming software | +| rollershutter | RW | | Rollershutter | blind | control rollershutter or blind. UpDown, StopMove and Percent command types are supported | +| measured | R | | Number:Temperature | thermostat | current temperature. Because of API restrictions, NHC II will only report in 0.5°C increments | +| heatingmode | RW | | String | thermostat | current thermostat mode. Allowed values are Day, Night, Eco, Off, Cool, Prog1, Prog2, Prog3. Setting `heatingmode` will reset the `setpoint` channel to the standard value for the mode in the controller | +| mode | RW | X | Number | thermostat | current thermostat mode, same meaning as `heatingmode`, but numeric values (0=Day, 1=Night, 2=Eco, 3=Off, 4=Cool, 5=Prog1, 6=Prog2, 7=Prog3). Setting `mode` will reset the `setpoint` channel to the standard value for the mode in the controller. This channel is kept for binding backward compatibility | +| setpoint | RW | | Number:Temperature | thermostat | current thermostat setpoint. Updating the `setpoint` will overrule the temperature setpoint defined by `heatingmode` or `mode` for `overruletime` duration. Because of API restrictions, NHC II will only report in 0.5°C increments | +| overruletime | RW | | Number | thermostat | used to set the total duration in minutes to apply the setpoint temperature set in the `setpoint` channel before the thermostat returns to the setting from its mode | +| heatingdemand | R | | String | thermostat | indicating if the system is actively heating/cooling. This channel will have value Heating, Cooling or None. For NHC I this is set by the binding from the temperature difference between `setpoint` and `measured`. It therefore may incorrectly indicate cooling even when the system does not have active cooling capabilities | +| demand | R | X | Number | thermostat | indicating if the system is actively heating/cooling, same as `heatingdemand` but numeric values (-1=Cooling, 0=None, 1=Heating) | +| power | R | | Number:Power | energyMeterLive | instant power consumption/production (negative for production), refreshed every 2s. Linking this channel starts an intensive communication flow with the controller and should only be done when appropriate | +| powerFromGrid | R | | Number:Power | energyMeterLive | power consumption grid for home energy meter, refreshed every 2s. Linking this channel starts an intensive communication flow with the controller and should only be done when appropriate | +| powerToGrid | R | | Number:Power | energyMeterLive | power sent to grid for home energy meter, refreshed every 2s. Linking this channel starts an intensive communication flow with the controller and should only be done when appropriate | +| peakPowerFromGrid | R | | Number:Power | energyMeterLive | current month peak power as registered by the home energy meter | +| energy | R | | Number:Energy | energyMeterLive, energyMeter | total energy meter reading | +| energyday | R | | Number:Energy | energyMeterLive, energyMeter | day energy meter reading | +| gas | R | | Number:Volume | gasMeter | total gas meter reading | +| gasday | R | | Number:Volume | gasMeter | day gas meter reading | +| water | R | | Number:Volume | waterMeter | total water meter reading | +| waterday | R | | Number:Volume | waterMeter | day water meter reading | +| measurementtime | R | | DateTimeType | energyMeterLive, energyMeter, gasMeter, waterMeter | last meter reading time | +| bellbutton | RW | | Switch | access, accessRingAndComeIn | bell button connected to access device, including buttons on video phone devices linked to an access device. The bell can also be triggered by an `ON` command, `autoupdate="false"` by default | +| ringandcomein | RW | | Switch | accessRingAndComeIn | provide state and turn automatic door unlocking at bell ring on/off | +| lock | RW | | Switch | access, accessRingAndComeIn | provide doorlock state and unlock the door by sending an `OFF` command. `autoupdate="false"` by default | +| arm | RW | | Switch | alarm | arm/disarm alarm, will change state (on/off) immediately. Note some integrations (Homekit, Google Home, ...) may require String states for an alarm system (ARMED/DISARMED). This can be achieved using an extra item and a rule updated by/commanding an item linked to this channel | +| armed | RW | | Switch | alarm | state of the alarm system (on/off), will only turn on after pre-armed period when arming | +| state | R | | String | alarm | state of the alarm system (DISARMED, PREARMED, ARMED, PREALARM, ALARM, DETECTOR PROBLEM) | +| status | RW | | Switch | carCharger | status of the car charger (on/off) | +| chargingStatus | R | | String | carCharger | charging status of the car charger (ACTIVE, INACTIVE, BATTERY FULL or ERROR) | +| evStatus | R | | String | carCharger | status of the electric vehicle (IDLE, CONNECTED or CHARGING) | +| couplingStatus | R | | String | carCharger | coupling status (OK, NO INTERNET, NO CREDENTIALS, INVALID CREDENTIALS, CONNECTION ERROR, CONNECTION TIMEOUT, API ERROR or UNKNOWN ERROR) | +| electricalPower | R | | Number:Power | carCharger | current charging power | +| chargingMode | RW | | String | carCharger | charging mode (SOLAR, NORMAL or SMART) | +| targetDistance | RW | | Number:Length | carCharger | target distance to achieve in charging activity | +| targetTime | RW | | DateTime | carCharger | time by which the target distance should be achieved | +| boost | RW | | Switch | carCharger | boost charging to maximum achievable, not respecting capacity limit | | reachableDistance | R | | Number:Length | carCharger | reachable distance in current charing activity | -| nextChargingTime | R | | DateTime | carCharger | next charging start in current charging activity | -| alarm | | | | bridge, alarm | trigger channel with alarm event message, can be used in rules | -| notice | | | | bridge | trigger channel with notice event message, can be used in rules | +| nextChargingTime | R | | DateTime | carCharger | next charging start in current charging activity | +| alarm | | | | bridge, alarm | trigger channel with alarm event message, can be used in rules | +| notice | | | | bridge | trigger channel with notice event message, can be used in rules | ## Console Commands diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/NikoHomeControlBindingConstants.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/NikoHomeControlBindingConstants.java index 0e94cf6a8f35d..dedd2ca986c31 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/NikoHomeControlBindingConstants.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/NikoHomeControlBindingConstants.java @@ -86,6 +86,9 @@ public class NikoHomeControlBindingConstants { public static final String CHANNEL_HEATING_DEMAND = "heatingdemand"; public static final String CHANNEL_POWER = "power"; + public static final String CHANNEL_POWER_FROM_GRID = "powerfromgrid"; + public static final String CHANNEL_POWER_TO_GRID = "powertogrid"; + public static final String CHANNEL_PEAK_POWER_FROM_GRID = "peakpowerfromgrid"; public static final String CHANNEL_ENERGY = "energy"; public static final String CHANNEL_GAS = "gas"; public static final String CHANNEL_WATER = "water"; diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlMeterHandler.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlMeterHandler.java index 5a5aabb0b52fc..04bce91c196ff 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlMeterHandler.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlMeterHandler.java @@ -50,6 +50,7 @@ * sent to one of the channels. * * @author Mark Herwege - Initial Contribution + * @author Mark Herwege - Add home digital meter power readings */ @NonNullByDefault public class NikoHomeControlMeterHandler extends NikoHomeControlBaseHandler implements NhcMeterEvent { @@ -172,7 +173,7 @@ void refresh() { nhcComm.startMeter(deviceId, getConfig().as(NikoHomeControlMeterConfig.class).refresh); // Subscribing to power readings starts an intensive data flow, therefore only do it when there is an item // linked to the channel - if (isLinked(CHANNEL_POWER)) { + if (isLinked(CHANNEL_POWER) || isLinked(CHANNEL_POWER_TO_GRID) || isLinked(CHANNEL_POWER_FROM_GRID)) { nhcComm.startMeterLive(deviceId); } } @@ -213,6 +214,12 @@ private void updateProperties(NhcMeter nhcMeter) { @Override public void meterPowerEvent(@Nullable Integer power) { + meterPowerEvent(power, null, null); + } + + @Override + public void meterPowerEvent(@Nullable Integer power, @Nullable Integer powerFromGrid, + @Nullable Integer powerToGrid) { NhcMeter nhcMeter = this.nhcMeter; if (nhcMeter == null) { logger.debug("meter with ID {} not initialized", deviceId); @@ -232,6 +239,28 @@ public void meterPowerEvent(@Nullable Integer power) { int value = (invert ? -1 : 1) * power; updateState(CHANNEL_POWER, new QuantityType<>(value, Units.WATT)); } + if (powerFromGrid == null) { + updateState(CHANNEL_POWER_FROM_GRID, UnDefType.UNDEF); + } else { + updateState(CHANNEL_POWER_FROM_GRID, new QuantityType<>(powerFromGrid, Units.WATT)); + } + if (powerToGrid == null) { + updateState(CHANNEL_POWER_TO_GRID, UnDefType.UNDEF); + } else { + updateState(CHANNEL_POWER_TO_GRID, new QuantityType<>(powerToGrid, Units.WATT)); + } + updateStatus(ThingStatus.ONLINE); + } + + @Override + public void meterPeakPowerFromGridEvent(int peakPowerFromGrid) { + NhcMeter nhcMeter = this.nhcMeter; + if (nhcMeter == null) { + logger.debug("meter with ID {} not initialized", deviceId); + return; + } + + updateState(CHANNEL_PEAK_POWER_FROM_GRID, new QuantityType<>(peakPowerFromGrid, Units.WATT)); updateStatus(ThingStatus.ONLINE); } @@ -285,7 +314,9 @@ public void meterReadingEvent(double meterReading, double meterReadingDay, Local public void channelLinked(ChannelUID channelUID) { // Subscribing to power readings starts an intensive data flow, therefore only do it when there is an item // linked to the channel - if (!CHANNEL_POWER.equals(channelUID.getId())) { + String channelId = channelUID.getId(); + if (!CHANNEL_POWER.equals(channelId) || CHANNEL_POWER_FROM_GRID.equals(channelId) + || CHANNEL_POWER_TO_GRID.equals(channelId)) { return; } NikoHomeControlCommunication nhcComm = getCommunication(getBridgeHandler()); diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcMeter.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcMeter.java index 376699ea03e6c..a963dd4ddca01 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcMeter.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcMeter.java @@ -29,6 +29,7 @@ * representing a Niko Home Control meter and has methods to receive meter usage information. * * @author Mark Herwege - Initial Contribution + * @author Mark Herwege - Add home digital meter power readings */ @NonNullByDefault public abstract class NhcMeter { @@ -45,6 +46,9 @@ public abstract class NhcMeter { // This can be null as long as we do not receive power readings protected volatile @Nullable Integer power; + protected volatile @Nullable Integer powerFromGrid; + protected volatile @Nullable Integer powerToGrid; + protected volatile int peakPowerFromGrid; protected volatile int reading; protected volatile int dayReading; protected volatile @Nullable LocalDateTime lastReadingUTC; @@ -75,10 +79,24 @@ protected NhcMeter(String id, String name, MeterType type, @Nullable LocalDateTi */ protected void updatePowerState() { NhcMeterEvent handler = eventHandler; - Integer value = getPower(); + Integer power = getPower(); + if ((handler != null) && (power != null)) { + logger.debug("update power channel for {} with {}", id, power); + handler.meterPowerEvent(power, getPowerFromGrid(), getPowerToGrid()); + } + } + + /** + * Update power value of the meter without touching the meter definition (id, name) and without changing the + * ThingHandler callback. + * + */ + protected void updatePeakPowerFromGridState() { + NhcMeterEvent handler = eventHandler; + Integer value = getPeakPowerFromGrid(); if ((handler != null) && (value != null)) { - logger.debug("update power channel for {} with {}", id, value); - handler.meterPowerEvent(value); + logger.debug("update peakPowerFromGrid channel for {} with {}", id, value); + handler.meterPeakPowerFromGridEvent(value); } } @@ -187,15 +205,57 @@ public void setLocation(@Nullable String location) { return power; } + /** + * @return the power consumed from the grid in W, return null if no reading received yet + */ + public @Nullable Integer getPowerFromGrid() { + return powerFromGrid; + } + + /** + * @return the power in W (positive for consumption, negative for production), return null if no reading received + * yet + */ + public @Nullable Integer getPowerToGrid() { + return powerToGrid; + } + /** * @param power the power to set in W (positive for consumption, negative for production), null if an empty reading * was received */ public void setPower(@Nullable Integer power) { + setPower(power, null, null); + } + + /** + * @param power the power to set in W (positive for consumption, negative for production), null if an empty reading + * was received + * @param powerFromGrid power consumed from the grid + * @param powerToGrid power sent to the grid + */ + public void setPower(@Nullable Integer power, @Nullable Integer powerFromGrid, @Nullable Integer powerToGrid) { this.power = power; + this.powerFromGrid = powerFromGrid; + this.powerToGrid = powerToGrid; updatePowerState(); } + /** + * @return the peak power for the current month in W + */ + public int getPeakPowerFromGrid() { + return peakPowerFromGrid; + } + + /** + * @param peakPowerFromGrid the power to set in W + */ + public void setPeakPowerFromGrid(int peakPowerFromGrid) { + this.peakPowerFromGrid = peakPowerFromGrid; + updatePeakPowerFromGridState(); + } + /** * @return the meter reading in the base unit (m^3 for gas and water, kWh for energy) */ diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcMeterEvent.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcMeterEvent.java index 652e53eb76337..d6538fc01c8cd 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcMeterEvent.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcMeterEvent.java @@ -35,6 +35,27 @@ public interface NhcMeterEvent extends NhcBaseEvent { */ void meterPowerEvent(@Nullable Integer power); + /** + * This method is called when a meter event is received from the Niko Home Control controller and separate + * information is available for power to grid and power from grid. + * + * @param power current power consumption/production in W (positive for consumption), null for an empty reading + * @param powerFromGrid current power consumption from grid in W, null for an empty reading + * @param powerToGrid current power sent to grid in W, null for an empty reading + */ + default void meterPowerEvent(@Nullable Integer power, @Nullable Integer powerFromGrid, + @Nullable Integer powerToGrid) { + meterPowerEvent(power); + } + + /** + * This method is called when a meter peak power from grid event is received from the Niko Home Control controller. + * + * @param peakPowerFromGrid current month peak power from grid + */ + default void meterPeakPowerFromGridEvent(int peakPowerFromGrid) { + } + /** * This method is called when a meter reading is received from the Niko Home Control controller. * diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcDevice2.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcDevice2.java index d14de570bb85d..bb939f8986344 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcDevice2.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcDevice2.java @@ -85,6 +85,8 @@ static class NhcProperty { @Nullable String electricalPowerFromGrid; @Nullable + String electricalMonthlyPeakPowerFromGrid; + @Nullable String electricalPowerProduction; @Nullable String electricalPowerSelfConsumption; diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NikoHomeControlCommunication2.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NikoHomeControlCommunication2.java index d1a5fcba096ff..f2378895de004 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NikoHomeControlCommunication2.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NikoHomeControlCommunication2.java @@ -74,6 +74,7 @@ * * @author Mark Herwege - Initial Contribution * @author Mark Herwege - Add car chargers + * @author Mark Herwege - Add home digital meter power readings */ @NonNullByDefault public class NikoHomeControlCommunication2 extends NikoHomeControlCommunication @@ -801,11 +802,24 @@ private void updateMeterState(NhcMeter2 meter, List devicePropertie .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null) .filter(Objects::nonNull).findFirst(); int power = electricalPower.orElse(powerFromGrid.orElse(0) - powerToGrid.orElse(0)); - logger.trace("setting energy meter {} power to {}", meter.getId(), power); - meter.setPower(power); + logger.trace("setting energy meter {} power to {}, powerFromGrid to {}, powerToGrid to {}", meter.getId(), + power, powerFromGrid.orElse(null), powerToGrid.orElse(null)); + meter.setPower(power, powerFromGrid.orElse(null), powerToGrid.orElse(null)); } catch (NumberFormatException e) { logger.trace("wrong format in energy meter {} power reading", meter.getId()); - meter.setPower(null); + meter.setPower(null, null, null); + } + + try { + Integer peakPowerFromGrid = deviceProperties.stream().map(p -> p.electricalMonthlyPeakPowerFromGrid) + .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null) + .filter(Objects::nonNull).findFirst().orElse(null); + if (peakPowerFromGrid != null) { + logger.trace("setting energy meter {} peakPowerFromGrid to {}", peakPowerFromGrid); + meter.setPeakPowerFromGrid(peakPowerFromGrid); + } + } catch (NumberFormatException e) { + logger.trace("wrong format in energy meter {} peakPowerFromGrid reading", meter.getId()); } } @@ -823,12 +837,12 @@ private void updateAccessState(NhcAccess2 accessDevice, List device } switch (accessDevice.getType()) { case RINGANDCOMEIN: - accessDevice.updateRingAndComeInState(state); logger.debug("setting access device {} ring and come in to {}", accessDevice.getId(), state); + accessDevice.updateRingAndComeInState(state); break; case BELLBUTTON: - accessDevice.updateBellState(state); logger.debug("setting access device {} bell to {}", accessDevice.getId(), state); + accessDevice.updateBellState(state); break; default: break; diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/i18n/nikohomecontrol.properties b/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/i18n/nikohomecontrol.properties index 9ab495936cbf9..6e9d1d4b580a2 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/i18n/nikohomecontrol.properties +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/i18n/nikohomecontrol.properties @@ -122,6 +122,12 @@ channelDemand1 = Heating channelPowerLabel = Power channelPowerDescription = Momentary power consumption/production (positive is consumption) +channelPowerFromGridLabel = Power From Grid +channelPowerFromGridDescription = Momentary power consumption from grid, only available for home energy meter +channelPowerToGridLabel = Power To Grid +channelPowerToGridDescription = Momentary power sent to grid, only available for home energy meter +channelPeakPowerFromGridLabel = Peak Power +channelPeakPowerFromGridDescription = Peak power from grid in current month, only available for home energy meter channelEnergyLabel = Energy channelEnergyDescription = Energy consumption/production (positive is consumption) diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml index 3e115a941cda5..7adf5b203b9bf 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml @@ -196,6 +196,18 @@ ElectricMeter + + + @text/channelPowerFromGridDescription + + + + @text/channelPowerToGridDescription + + + + @text/channelPeakPowerFromGridDescription + @@ -516,6 +528,16 @@ + + Number:Power + + @text/channelPowerDescription + + Measurement + Power + + + Number:Energy From 6baa00ac8886068c5f6f6c951aa8befe429b755b Mon Sep 17 00:00:00 2001 From: Mark Herwege Date: Fri, 10 Oct 2025 13:09:10 +0200 Subject: [PATCH 03/13] implement energy metering in NHCII Signed-off-by: Mark Herwege --- .../README.md | 27 +-- .../NikoHomeControlBindingConstants.java | 10 +- .../NikoHomeControlHandlerFactory.java | 8 +- .../NikoHomeControlDiscoveryService.java | 4 + .../handler/NikoHomeControlBaseHandler.java | 4 +- .../NikoHomeControlBridgeHandler2.java | 13 +- .../handler/NikoHomeControlMeterConfig.java | 1 + .../handler/NikoHomeControlMeterHandler.java | 132 +++++++++++- .../internal/protocol/NhcMeter.java | 95 +++++---- .../internal/protocol/NhcMeterEvent.java | 21 +- .../NikoHomeControlCommunication.java | 7 +- .../protocol/NikoHomeControlConstants.java | 13 ++ .../internal/protocol/nhc1/NhcMeter1.java | 37 ++++ .../nhc1/NikoHomeControlCommunication1.java | 19 +- .../internal/protocol/nhc2/NhcDevice2.java | 16 +- .../protocol/nhc2/NhcHttpConnection2.java | 104 ++++++++++ .../internal/protocol/nhc2/NhcMeter2.java | 56 +++++ .../protocol/nhc2/NhcMeterReading2.java | 46 +++++ .../nhc2/NikoHomeControlCommunication2.java | 191 ++++++++++++++---- .../OH-INF/i18n/nikohomecontrol.properties | 21 ++ .../resources/OH-INF/thing/thing-types.xml | 95 +++++++-- 21 files changed, 773 insertions(+), 147 deletions(-) create mode 100644 bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcHttpConnection2.java create mode 100644 bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcMeterReading2.java diff --git a/bundles/org.openhab.binding.nikohomecontrol/README.md b/bundles/org.openhab.binding.nikohomecontrol/README.md index b3c9950358074..9611891fbb8e0 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/README.md +++ b/bundles/org.openhab.binding.nikohomecontrol/README.md @@ -24,7 +24,7 @@ The installation only needs to be 'connected' (registered on the Niko Home Contr For Niko Home Control I, the binding exposes all actions from the Niko Home Control System that can be triggered from the smartphone/tablet interface, as defined in the Niko Home Control I programming software. For Niko Home Control II, the binding exposes all devices in the system. -Supported device types are switches, dimmers and rollershutters or blinds, thermostats, energy meters (Niko Home Control I only), access control (Niko Home Control II only) and car chargers (Niko Home Control II only). +Supported device types are switches, dimmers and rollershutters or blinds, thermostats, energy meters, access control (Niko Home Control II only) and car chargers (Niko Home Control II only). Niko Home Control alarm and notice messages are retrieved and made available in the binding. ## Supported Things @@ -41,14 +41,15 @@ The following thing types are available in the binding: | dimmer | x | x | dimmable light action | | blind | x | x | rollershutter, venetian blind | | thermostat | x | x | thermostat | +| energyMeterHome | | x | home digital energy meter | | energyMeterLive | x | x | energy meter with live power monitoring and aggregation | -| energyMeter | x | | energy meter, aggregates readings with 10 min intervals | -| gasMeter | x | | gas meter, aggregates readings with 10 min intervals | -| waterMeter | x | | water meter, aggregates readings with 10 min intervals | +| energyMeter | x | x | energy meter, aggregates readings with 10 min intervals | +| gasMeter | x | x | gas meter, aggregates readings with 10 min intervals | +| waterMeter | x | x | water meter, aggregates readings with 10 min intervals | | access | | x | door with bell button and lock | | accessRingAndComeIn | | x | door with bell button, lock and ring and come in functionality | | alarm | | x | alarm system | -| carCharger | x | x | car charger device | +| carCharger | | x | car charger device | ## Binding Configuration @@ -138,7 +139,7 @@ The Thing configurations for **Niko Home Control actions, thermostats, energy me | invert | x | x | | blind, energyMeterLive, energyMeter, gasMeter, waterMeter | inverts rollershutter or blind direction. Inverts sign of meter reading. Default false | | thermostatId | x | x | x | thermostat | unique ID for the thermostat in the controller | | overruleTime | x | x | | thermostat | standard overrule duration in minutes when setting a new setpoint without providing an overrule duration, default value is 60 | -| meterId | x | x | x | energyMeterLive, energyMeter, gasMeter, waterMeter | unique ID for the energy meter in the controller | +| meterId | x | x | x | energyMeterHome, energyMeterLive, energyMeter, gasMeter, waterMeter | unique ID for the energy meter in the controller | | refresh | x | x | | energyMeterLive, energyMeter, gasMeter, waterMeter | refresh interval for meter reading in minutes, default 10 minutes. The value should not be lower than 5 minutes to avoid too many meter data retrieval calls | | accessId | | x | x | access, accessRingAndComeIn | unique ID for the access device in the controller | | alarmId | | x | x | alarm | unique ID for the alarm system in the controller | @@ -207,17 +208,17 @@ Thing nikohomecontrol:carCharger:mybridge:mycarcharger [ carChargerId="abcdef01- | overruletime | RW | | Number | thermostat | used to set the total duration in minutes to apply the setpoint temperature set in the `setpoint` channel before the thermostat returns to the setting from its mode | | heatingdemand | R | | String | thermostat | indicating if the system is actively heating/cooling. This channel will have value Heating, Cooling or None. For NHC I this is set by the binding from the temperature difference between `setpoint` and `measured`. It therefore may incorrectly indicate cooling even when the system does not have active cooling capabilities | | demand | R | X | Number | thermostat | indicating if the system is actively heating/cooling, same as `heatingdemand` but numeric values (-1=Cooling, 0=None, 1=Heating) | -| power | R | | Number:Power | energyMeterLive | instant power consumption/production (negative for production), refreshed every 2s. Linking this channel starts an intensive communication flow with the controller and should only be done when appropriate | -| powerFromGrid | R | | Number:Power | energyMeterLive | power consumption grid for home energy meter, refreshed every 2s. Linking this channel starts an intensive communication flow with the controller and should only be done when appropriate | -| powerToGrid | R | | Number:Power | energyMeterLive | power sent to grid for home energy meter, refreshed every 2s. Linking this channel starts an intensive communication flow with the controller and should only be done when appropriate | -| peakPowerFromGrid | R | | Number:Power | energyMeterLive | current month peak power as registered by the home energy meter | -| energy | R | | Number:Energy | energyMeterLive, energyMeter | total energy meter reading | -| energyday | R | | Number:Energy | energyMeterLive, energyMeter | day energy meter reading | +| power | R | | Number:Power | energyMeterHome, energyMeterLive | instant power consumption/production (negative for production), refreshed every 2s. Linking this channel starts an intensive communication flow with the controller and should only be done when appropriate | +| powerFromGrid | R | | Number:Power | energyMeterHome | power consumption grid for home energy meter, refreshed every 2s. Linking this channel starts an intensive communication flow with the controller and should only be done when appropriate | +| powerToGrid | R | | Number:Power | energyMeterHome | power sent to grid for home energy meter, refreshed every 2s. Linking this channel starts an intensive communication flow with the controller and should only be done when appropriate | +| peakPowerFromGrid | R | | Number:Power | energyMeterHome | current month peak power as registered by the home energy meter | +| energy | R | | Number:Energy | energyMeterHome, energyMeterLive, energyMeter | total energy meter reading | +| energyday | R | | Number:Energy | energyMeterHome, energyMeterLive, energyMeter | day energy meter reading | | gas | R | | Number:Volume | gasMeter | total gas meter reading | | gasday | R | | Number:Volume | gasMeter | day gas meter reading | | water | R | | Number:Volume | waterMeter | total water meter reading | | waterday | R | | Number:Volume | waterMeter | day water meter reading | -| measurementtime | R | | DateTimeType | energyMeterLive, energyMeter, gasMeter, waterMeter | last meter reading time | +| measurementtime | R | | DateTimeType | energyMeterHome, energyMeterLive, energyMeter, gasMeter, waterMeter | last meter reading time | | bellbutton | RW | | Switch | access, accessRingAndComeIn | bell button connected to access device, including buttons on video phone devices linked to an access device. The bell can also be triggered by an `ON` command, `autoupdate="false"` by default | | ringandcomein | RW | | Switch | accessRingAndComeIn | provide state and turn automatic door unlocking at bell ring on/off | | lock | RW | | Switch | access, accessRingAndComeIn | provide doorlock state and unlock the door by sending an `OFF` command. `autoupdate="false"` by default | diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/NikoHomeControlBindingConstants.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/NikoHomeControlBindingConstants.java index dedd2ca986c31..423df54d3470c 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/NikoHomeControlBindingConstants.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/NikoHomeControlBindingConstants.java @@ -47,6 +47,7 @@ public class NikoHomeControlBindingConstants { public static final ThingTypeUID THING_TYPE_THERMOSTAT = new ThingTypeUID(BINDING_ID, "thermostat"); public static final ThingTypeUID THING_TYPE_ENERGYMETER_LIVE = new ThingTypeUID(BINDING_ID, "energyMeterLive"); public static final ThingTypeUID THING_TYPE_ENERGYMETER = new ThingTypeUID(BINDING_ID, "energyMeter"); + public static final ThingTypeUID THING_TYPE_ENERGYMETER_HOME = new ThingTypeUID(BINDING_ID, "energyMeterHome"); public static final ThingTypeUID THING_TYPE_GASMETER = new ThingTypeUID(BINDING_ID, "gasMeter"); public static final ThingTypeUID THING_TYPE_WATERMETER = new ThingTypeUID(BINDING_ID, "waterMeter"); public static final ThingTypeUID THING_TYPE_ACCESS = new ThingTypeUID(BINDING_ID, "access"); @@ -61,7 +62,7 @@ public class NikoHomeControlBindingConstants { THING_TYPE_ON_OFF_LIGHT, THING_TYPE_DIMMABLE_LIGHT, THING_TYPE_BLIND); public static final Set THERMOSTAT_THING_TYPES_UIDS = Set.of(THING_TYPE_THERMOSTAT); public static final Set METER_THING_TYPES_UIDS = Set.of(THING_TYPE_ENERGYMETER_LIVE, - THING_TYPE_ENERGYMETER, THING_TYPE_GASMETER, THING_TYPE_WATERMETER); + THING_TYPE_ENERGYMETER, THING_TYPE_ENERGYMETER_HOME, THING_TYPE_GASMETER, THING_TYPE_WATERMETER); public static final Set ACCESS_THING_TYPES_UIDS = Set.of(THING_TYPE_ACCESS, THING_TYPE_ACCESS_RINGANDCOMEIN); public static final Set ALARM_THING_TYPES_UIDS = Set.of(THING_TYPE_ALARM); @@ -90,11 +91,17 @@ public class NikoHomeControlBindingConstants { public static final String CHANNEL_POWER_TO_GRID = "powertogrid"; public static final String CHANNEL_PEAK_POWER_FROM_GRID = "peakpowerfromgrid"; public static final String CHANNEL_ENERGY = "energy"; + public static final String CHANNEL_ENERGY_FROM_GRID = "energyfromgrid"; + public static final String CHANNEL_ENERGY_TO_GRID = "energytogrid"; + public static final String CHANNEL_ENERGY_SELF_CONSUMPTION = "energyselfconsumption"; public static final String CHANNEL_GAS = "gas"; public static final String CHANNEL_WATER = "water"; public static final String CHANNEL_ENERGY_DAY = "energyday"; public static final String CHANNEL_GAS_DAY = "gasday"; public static final String CHANNEL_WATER_DAY = "waterday"; + public static final String CHANNEL_ENERGY_FROM_GRID_DAY = "energyfromgridday"; + public static final String CHANNEL_ENERGY_TO_GRID_DAY = "energytogridday"; + public static final String CHANNEL_ENERGY_SELF_CONSUMPTION_DAY = "energyselfconsumptionday"; public static final String CHANNEL_ENERGY_LAST = "energylast"; public static final String CHANNEL_GAS_LAST = "gaslast"; public static final String CHANNEL_WATER_LAST = "waterlast"; @@ -139,6 +146,7 @@ public class NikoHomeControlBindingConstants { public static final String METER_ID = "meterId"; public static final String CONFIG_METER_REFRESH = "refresh"; + public static final String CONFIG_METER_START_DATE = "startDate"; public static final String CONFIG_ACCESS_ID = "accessId"; diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/NikoHomeControlHandlerFactory.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/NikoHomeControlHandlerFactory.java index cf2b200aad35d..b02796570ddbb 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/NikoHomeControlHandlerFactory.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/NikoHomeControlHandlerFactory.java @@ -25,6 +25,7 @@ import org.openhab.binding.nikohomecontrol.internal.handler.NikoHomeControlMeterHandler; import org.openhab.binding.nikohomecontrol.internal.handler.NikoHomeControlThermostatHandler; import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.net.NetworkAddressService; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Thing; @@ -50,12 +51,14 @@ public class NikoHomeControlHandlerFactory extends BaseThingHandlerFactory { private final NetworkAddressService networkAddressService; private final TimeZoneProvider timeZoneProvider; + private final HttpClientFactory httpClientFactory; @Activate public NikoHomeControlHandlerFactory(final @Reference NetworkAddressService networkAddressService, - final @Reference TimeZoneProvider timeZoneProvider) { + final @Reference TimeZoneProvider timeZoneProvider, final @Reference HttpClientFactory httpClientFactory) { this.networkAddressService = networkAddressService; this.timeZoneProvider = timeZoneProvider; + this.httpClientFactory = httpClientFactory; } @Override @@ -67,7 +70,8 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { protected @Nullable ThingHandler createHandler(Thing thing) { if (BRIDGE_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { if (BRIDGEII_THING_TYPE.equals(thing.getThingTypeUID())) { - return new NikoHomeControlBridgeHandler2((Bridge) thing, networkAddressService, timeZoneProvider); + return new NikoHomeControlBridgeHandler2((Bridge) thing, networkAddressService, timeZoneProvider, + httpClientFactory); } else { return new NikoHomeControlBridgeHandler1((Bridge) thing, networkAddressService, timeZoneProvider); } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/discovery/NikoHomeControlDiscoveryService.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/discovery/NikoHomeControlDiscoveryService.java index f8232e35dd941..3a9295c1ef7ae 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/discovery/NikoHomeControlDiscoveryService.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/discovery/NikoHomeControlDiscoveryService.java @@ -146,6 +146,10 @@ private void discoverMeterDevices(NikoHomeControlBridgeHandler bridgeHandler, addDevice(new ThingUID(THING_TYPE_ENERGYMETER_LIVE, bridgeHandler.getThing().getUID(), deviceId), METER_ID, deviceId, thingName, thingLocation); break; + case ENERGY_HOME: + addDevice(new ThingUID(THING_TYPE_ENERGYMETER_HOME, bridgeHandler.getThing().getUID(), deviceId), + METER_ID, deviceId, thingName, thingLocation); + break; case ENERGY: addDevice(new ThingUID(THING_TYPE_ENERGYMETER, bridgeHandler.getThing().getUID(), deviceId), METER_ID, deviceId, thingName, thingLocation); diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBaseHandler.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBaseHandler.java index 83f50db6c7b97..0c9bb7a4409a5 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBaseHandler.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBaseHandler.java @@ -52,9 +52,10 @@ public NikoHomeControlBaseHandler(Thing thing) { @Override public void dispose() { + Future commStartThread = this.commStartThread; if (commStartThread != null) { commStartThread.cancel(true); - commStartThread = null; + this.commStartThread = null; } super.dispose(); } @@ -141,6 +142,7 @@ public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) { ThingStatus bridgeStatus = bridgeStatusInfo.getStatus(); if (ThingStatus.ONLINE.equals(bridgeStatus)) { if (!initialized) { + Future commStartThread = this.commStartThread; if (commStartThread != null) { commStartThread.cancel(true); } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBridgeHandler2.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBridgeHandler2.java index 85c92a7de6909..d192877585e58 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBridgeHandler2.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBridgeHandler2.java @@ -23,8 +23,10 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; import org.openhab.binding.nikohomecontrol.internal.protocol.nhc2.NikoHomeControlCommunication2; import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.net.NetworkAddressService; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ThingStatus; @@ -49,9 +51,12 @@ public class NikoHomeControlBridgeHandler2 extends NikoHomeControlBridgeHandler private final Gson gson = new GsonBuilder().create(); + private final HttpClient httpClient; + public NikoHomeControlBridgeHandler2(Bridge nikoHomeControlBridge, NetworkAddressService networkAddressService, - TimeZoneProvider timeZoneProvider) { + TimeZoneProvider timeZoneProvider, HttpClientFactory httpClientFactory) { super(nikoHomeControlBridge, networkAddressService, timeZoneProvider); + httpClient = httpClientFactory.getCommonHttpClient(); } @Override @@ -85,7 +90,7 @@ public void initialize() { addr = (addr == null) ? "unknown" : addr.replace(".", "_"); String clientId = addr + "-" + thing.getUID().toString().replace(":", "_"); try { - nhcComm = new NikoHomeControlCommunication2(this, clientId, scheduler); + nhcComm = new NikoHomeControlCommunication2(this, clientId, scheduler, httpClient); startCommunication(); } catch (CertificateException e) { // this should not happen unless there is a programming error @@ -172,6 +177,10 @@ public String getToken() { return token; } + public HttpClient getHttpClient() { + return httpClient; + } + /** * Extract the expiry date in the user provided token for the hobby API. Log warnings and errors if the token is * close to expiry or expired. diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlMeterConfig.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlMeterConfig.java index 4e765235a4ab4..940a7309fa967 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlMeterConfig.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlMeterConfig.java @@ -23,5 +23,6 @@ public class NikoHomeControlMeterConfig { public String meterId = ""; public int refresh = 10; + public String startDate = ""; public boolean invert = false; } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlMeterHandler.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlMeterHandler.java index 04bce91c196ff..ca9697e174646 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlMeterHandler.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlMeterHandler.java @@ -16,21 +16,27 @@ import static org.openhab.core.library.unit.Units.KILOWATT_HOUR; import static org.openhab.core.types.RefreshType.REFRESH; +import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoUnit; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.nikohomecontrol.internal.protocol.NhcMeter; import org.openhab.binding.nikohomecontrol.internal.protocol.NhcMeterEvent; import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlCommunication; +import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlConstants; import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlConstants.MeterType; import org.openhab.binding.nikohomecontrol.internal.protocol.nhc1.NhcMeter1; import org.openhab.binding.nikohomecontrol.internal.protocol.nhc2.NhcMeter2; +import org.openhab.core.config.core.Configuration; import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.unit.SIUnits; @@ -61,6 +67,8 @@ public class NikoHomeControlMeterHandler extends NikoHomeControlBaseHandler impl private volatile @Nullable NhcMeter nhcMeter; + private final Map powerChannelLinked = new ConcurrentHashMap<>(); + public NikoHomeControlMeterHandler(Thing thing) { super(thing); } @@ -151,6 +159,27 @@ synchronized void startCommunication() { return; } + String startDate = getConfig().as(NikoHomeControlMeterConfig.class).startDate; + if (startDate.isEmpty()) { + LocalDateTime referenceDate = nhcMeter.getReferenceDate(); + if (referenceDate != null) { + startDate = referenceDate.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); + } else { + startDate = Instant.now().atZone(nhcComm.getTimeZone()).truncatedTo(ChronoUnit.DAYS) + .format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); + } + Configuration config = editConfiguration(); + config.put(CONFIG_METER_START_DATE, startDate); + updateConfiguration(config); + } + try { + LocalDateTime.parse(startDate); + } catch (DateTimeParseException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.configuration-error.meterStartDate"); + return; + } + nhcMeter.setEventHandler(this); updateProperties(nhcMeter); @@ -168,9 +197,17 @@ synchronized void startCommunication() { @Override void refresh() { + NhcMeter meter = nhcMeter; + if (meter != null) { + Double peakPower = meter.getPeakPowerFromGrid(); + if (peakPower != null) { + meterPeakPowerFromGridEvent(peakPower); + } + } NikoHomeControlCommunication nhcComm = getCommunication(getBridgeHandler()); if (nhcComm != null) { - nhcComm.startMeter(deviceId, getConfig().as(NikoHomeControlMeterConfig.class).refresh); + NikoHomeControlMeterConfig config = getConfig().as(NikoHomeControlMeterConfig.class); + nhcComm.startMeter(deviceId, config.refresh, config.startDate); // Subscribing to power readings starts an intensive data flow, therefore only do it when there is an item // linked to the channel if (isLinked(CHANNEL_POWER) || isLinked(CHANNEL_POWER_TO_GRID) || isLinked(CHANNEL_POWER_FROM_GRID)) { @@ -213,13 +250,12 @@ private void updateProperties(NhcMeter nhcMeter) { } @Override - public void meterPowerEvent(@Nullable Integer power) { + public void meterPowerEvent(@Nullable Double power) { meterPowerEvent(power, null, null); } @Override - public void meterPowerEvent(@Nullable Integer power, @Nullable Integer powerFromGrid, - @Nullable Integer powerToGrid) { + public void meterPowerEvent(@Nullable Double power, @Nullable Double powerFromGrid, @Nullable Double powerToGrid) { NhcMeter nhcMeter = this.nhcMeter; if (nhcMeter == null) { logger.debug("meter with ID {} not initialized", deviceId); @@ -236,7 +272,7 @@ public void meterPowerEvent(@Nullable Integer power, @Nullable Integer powerFrom updateState(CHANNEL_POWER, UnDefType.UNDEF); } else { boolean invert = getConfig().as(NikoHomeControlMeterConfig.class).invert; - int value = (invert ? -1 : 1) * power; + double value = (invert ? -1 : 1) * power; updateState(CHANNEL_POWER, new QuantityType<>(value, Units.WATT)); } if (powerFromGrid == null) { @@ -253,7 +289,7 @@ public void meterPowerEvent(@Nullable Integer power, @Nullable Integer powerFrom } @Override - public void meterPeakPowerFromGridEvent(int peakPowerFromGrid) { + public void meterPeakPowerFromGridEvent(double peakPowerFromGrid) { NhcMeter nhcMeter = this.nhcMeter; if (nhcMeter == null) { logger.debug("meter with ID {} not initialized", deviceId); @@ -310,17 +346,85 @@ public void meterReadingEvent(double meterReading, double meterReadingDay, Local } } + @Override + public void meterReadingEvent(Map meterReadings, Map meterReadingsDay, + LocalDateTime lastReadingUTC) { + NhcMeter nhcMeter = this.nhcMeter; + if (nhcMeter == null) { + logger.debug("meter with ID {} not initialized", deviceId); + return; + } + + NikoHomeControlBridgeHandler bridgeHandler = getBridgeHandler(); + if (bridgeHandler == null) { + logger.debug("Cannot update meter channels, no bridge handler"); + return; + } + + boolean invert = getConfig().as(NikoHomeControlMeterConfig.class).invert; + meterReadings.forEach((reading, v) -> { + if (v != null) { + double value = (invert ? -1 : 1) * v; + Double dayValue = meterReadingsDay.get(reading); + if (dayValue != null) { + dayValue = (invert ? -1 : 1) * dayValue; + } + switch (reading) { + case NikoHomeControlConstants.NHC_ELECTRICAL_ENERGY: + case NikoHomeControlConstants.NHC_ELECTRICAL_ENERGY_CONSUMPTION: + updateState(CHANNEL_ENERGY, new QuantityType<>(value, KILOWATT_HOUR)); + if (dayValue != null) { + updateState(CHANNEL_ENERGY_DAY, new QuantityType<>(value, KILOWATT_HOUR)); + } + break; + case NikoHomeControlConstants.NHC_ELECTRICAL_ENERGY_FROM_GRID: + updateState(CHANNEL_ENERGY_FROM_GRID, new QuantityType<>(value, KILOWATT_HOUR)); + if (dayValue != null) { + updateState(CHANNEL_ENERGY_FROM_GRID_DAY, new QuantityType<>(value, KILOWATT_HOUR)); + } + break; + case NikoHomeControlConstants.NHC_ELECTRICAL_ENERGY_TO_GRID: + updateState(CHANNEL_ENERGY_TO_GRID, new QuantityType<>(value, KILOWATT_HOUR)); + if (dayValue != null) { + updateState(CHANNEL_ENERGY_TO_GRID_DAY, new QuantityType<>(value, KILOWATT_HOUR)); + } + break; + case NikoHomeControlConstants.NHC_ELECTRICAL_ENERGY_SELF_CONSUMPTION: + updateState(CHANNEL_ENERGY_SELF_CONSUMPTION, new QuantityType<>(value, KILOWATT_HOUR)); + if (dayValue != null) { + updateState(CHANNEL_ENERGY_SELF_CONSUMPTION_DAY, new QuantityType<>(value, KILOWATT_HOUR)); + } + break; + case NikoHomeControlConstants.NHC_GAS_VOLUME: + updateState(CHANNEL_GAS, new QuantityType<>(value, SIUnits.CUBIC_METRE)); + if (dayValue != null) { + updateState(CHANNEL_GAS_DAY, new QuantityType<>(value, SIUnits.CUBIC_METRE)); + } + break; + case NikoHomeControlConstants.NHC_WATER_VOLUME: + updateState(CHANNEL_WATER, new QuantityType<>(value, SIUnits.CUBIC_METRE)); + if (dayValue != null) { + updateState(CHANNEL_WATER_DAY, new QuantityType<>(value, SIUnits.CUBIC_METRE)); + } + break; + default: + break; + } + } + }); + } + @Override public void channelLinked(ChannelUID channelUID) { // Subscribing to power readings starts an intensive data flow, therefore only do it when there is an item // linked to the channel String channelId = channelUID.getId(); - if (!CHANNEL_POWER.equals(channelId) || CHANNEL_POWER_FROM_GRID.equals(channelId) - || CHANNEL_POWER_TO_GRID.equals(channelId)) { + if (!(CHANNEL_POWER.equals(channelId) || CHANNEL_POWER_FROM_GRID.equals(channelId) + || CHANNEL_POWER_TO_GRID.equals(channelId))) { return; } NikoHomeControlCommunication nhcComm = getCommunication(getBridgeHandler()); - if (nhcComm != null) { + if (nhcComm != null && !powerChannelLinked.values().stream().anyMatch(Boolean::booleanValue)) { // This can be expensive, therefore do it in a job. scheduler.submit(() -> { if (!nhcComm.communicationActive()) { @@ -332,15 +436,19 @@ public void channelLinked(ChannelUID channelUID) { } }); } + powerChannelLinked.put(channelId, true); } @Override public void channelUnlinked(ChannelUID channelUID) { - if (!CHANNEL_POWER.equals(channelUID.getId())) { + String channelId = channelUID.getId(); + if (!(CHANNEL_POWER.equals(channelId) || CHANNEL_POWER_FROM_GRID.equals(channelId) + || CHANNEL_POWER_TO_GRID.equals(channelId))) { return; } + powerChannelLinked.put(channelId, false); NikoHomeControlCommunication nhcComm = getCommunication(getBridgeHandler()); - if (nhcComm != null) { + if (nhcComm != null && !powerChannelLinked.values().stream().anyMatch(Boolean::booleanValue)) { // This can be expensive, therefore do it in a job. scheduler.submit(() -> { if (!nhcComm.communicationActive()) { @@ -352,6 +460,8 @@ public void channelUnlinked(ChannelUID channelUID) { // as this is momentary power production/consumption, we set it UNDEF as we do not get readings // anymore updateState(CHANNEL_POWER, UnDefType.UNDEF); + updateState(CHANNEL_POWER_FROM_GRID, UnDefType.UNDEF); + updateState(CHANNEL_POWER_TO_GRID, UnDefType.UNDEF); } }); } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcMeter.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcMeter.java index a963dd4ddca01..ee25be8cba868 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcMeter.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcMeter.java @@ -13,6 +13,7 @@ package org.openhab.binding.nikohomecontrol.internal.protocol; import java.time.LocalDateTime; +import java.util.Map; import java.util.Random; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; @@ -45,15 +46,13 @@ public abstract class NhcMeter { protected @Nullable String location; // This can be null as long as we do not receive power readings - protected volatile @Nullable Integer power; - protected volatile @Nullable Integer powerFromGrid; - protected volatile @Nullable Integer powerToGrid; - protected volatile int peakPowerFromGrid; - protected volatile int reading; - protected volatile int dayReading; + protected volatile @Nullable Double power; + protected volatile @Nullable Double powerFromGrid; + protected volatile @Nullable Double powerToGrid; + protected volatile @Nullable Double peakPowerFromGrid; protected volatile @Nullable LocalDateTime lastReadingUTC; - private @Nullable NhcMeterEvent eventHandler; + protected @Nullable NhcMeterEvent eventHandler; private ScheduledExecutorService scheduler; private volatile @Nullable ScheduledFuture restartTimer; @@ -79,10 +78,12 @@ protected NhcMeter(String id, String name, MeterType type, @Nullable LocalDateTi */ protected void updatePowerState() { NhcMeterEvent handler = eventHandler; - Integer power = getPower(); + Double power = getPower(); + Double powerFromGrid = getPowerFromGrid(); + Double powerToGrid = getPowerToGrid(); if ((handler != null) && (power != null)) { - logger.debug("update power channel for {} with {}", id, power); - handler.meterPowerEvent(power, getPowerFromGrid(), getPowerToGrid()); + logger.debug("update power channels for {} with {}, {}, {}", id, power, powerFromGrid, powerToGrid); + handler.meterPowerEvent(power, powerFromGrid, powerToGrid); } } @@ -93,8 +94,8 @@ protected void updatePowerState() { */ protected void updatePeakPowerFromGridState() { NhcMeterEvent handler = eventHandler; - Integer value = getPeakPowerFromGrid(); - if ((handler != null) && (value != null)) { + Double value = getPeakPowerFromGrid(); + if (handler != null && value != null) { logger.debug("update peakPowerFromGrid channel for {} with {}", id, value); handler.meterPeakPowerFromGridEvent(value); } @@ -201,14 +202,14 @@ public void setLocation(@Nullable String location) { * @return the power in W (positive for consumption, negative for production), return null if no reading received * yet */ - public @Nullable Integer getPower() { + public @Nullable Double getPower() { return power; } /** * @return the power consumed from the grid in W, return null if no reading received yet */ - public @Nullable Integer getPowerFromGrid() { + public @Nullable Double getPowerFromGrid() { return powerFromGrid; } @@ -216,7 +217,7 @@ public void setLocation(@Nullable String location) { * @return the power in W (positive for consumption, negative for production), return null if no reading received * yet */ - public @Nullable Integer getPowerToGrid() { + public @Nullable Double getPowerToGrid() { return powerToGrid; } @@ -224,7 +225,7 @@ public void setLocation(@Nullable String location) { * @param power the power to set in W (positive for consumption, negative for production), null if an empty reading * was received */ - public void setPower(@Nullable Integer power) { + public void setPower(@Nullable Double power) { setPower(power, null, null); } @@ -234,7 +235,7 @@ public void setPower(@Nullable Integer power) { * @param powerFromGrid power consumed from the grid * @param powerToGrid power sent to the grid */ - public void setPower(@Nullable Integer power, @Nullable Integer powerFromGrid, @Nullable Integer powerToGrid) { + public void setPower(@Nullable Double power, @Nullable Double powerFromGrid, @Nullable Double powerToGrid) { this.power = power; this.powerFromGrid = powerFromGrid; this.powerToGrid = powerToGrid; @@ -244,50 +245,60 @@ public void setPower(@Nullable Integer power, @Nullable Integer powerFromGrid, @ /** * @return the peak power for the current month in W */ - public int getPeakPowerFromGrid() { + public @Nullable Double getPeakPowerFromGrid() { return peakPowerFromGrid; } /** * @param peakPowerFromGrid the power to set in W */ - public void setPeakPowerFromGrid(int peakPowerFromGrid) { - this.peakPowerFromGrid = peakPowerFromGrid; - updatePeakPowerFromGridState(); + public void setPeakPowerFromGrid(@Nullable Double peakPowerFromGrid) { + if (peakPowerFromGrid != null) { + this.peakPowerFromGrid = peakPowerFromGrid; + updatePeakPowerFromGridState(); + } } /** * @return the meter reading in the base unit (m^3 for gas and water, kWh for energy) */ public double getReading() { - // For energy, readings are in W per 10 min, convert to kWh - // For water and gas, readings are in 0.1 dm^3, convert to m^3 - return ((type == MeterType.ENERGY) || (type == MeterType.ENERGY_LIVE)) ? (reading / 6000.0) - : (reading / 10000.0); + return 0; + } + + /** + * @return the meter readings in the base unit (m^3 for gas and water, kWh for energy) + */ + public Map getReadings() { + return Map.of(); } /** * @return reading without conversion, as provided by NHC */ - public int getReadingInt() { - return reading; + public double getReadingRaw() { + return 0; } /** * @return the meter reading for the current day in the base unit (m^3 for gas and water, kWh for energy) */ public double getDayReading() { - // For energy, readings are in W per 10 min, convert to kWh - // For water and gas, readings are in 0.1 dm^3, convert to m^3 - return ((type == MeterType.ENERGY) || (type == MeterType.ENERGY_LIVE)) ? (dayReading / 6000.0) - : (dayReading / 10000.0); + return 0; + } + + /** + * @return the meter readings for the current day in the base unit (m^3 for gas and water, kWh for energy) + */ + public Map getDayReadings() { + return Map.of(); } /** * @return day reading without conversion, as provided by NHC */ - public int getDayReadingInt() { - return dayReading; + public double getDayReadingRaw() { + return 0; } /** @@ -302,11 +313,15 @@ public int getDayReadingInt() { * @param dayReading the day meter reading * @param lastReading the last meter reading time in UTC zone */ - public void setReading(int reading, int dayReading, LocalDateTime lastReading) { - this.reading = reading; - this.dayReading = dayReading; - this.lastReadingUTC = lastReading; - updateReadingState(); + public void setReading(double reading, double dayReading, LocalDateTime lastReading) { + } + + /** + * @param readings the meter readings + * @param dayReadings the day meter readings + * @param lastReading the last meter reading time in UTC zone + */ + public void setReadings(Map readings, Map dayReadings, LocalDateTime lastReading) { } /** @@ -340,13 +355,13 @@ public void stopMeterLive() { * * @param refresh interval between meter queries in minutes */ - public void startMeter(int refresh) { + public void startMeter(int refresh, String startDate) { stopMeter(); int firstRefreshDelay = 10 + r.nextInt(90); logger.debug("schedule meter data refresh for {} every {} minutes, first refresh in {}s", id, refresh, firstRefreshDelay); readingSchedule = scheduler.scheduleWithFixedDelay(() -> { - nhcComm.executeMeter(id); + nhcComm.executeMeter(id, startDate); }, firstRefreshDelay, refresh * 60, TimeUnit.SECONDS); } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcMeterEvent.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcMeterEvent.java index d6538fc01c8cd..19681cb38ae41 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcMeterEvent.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcMeterEvent.java @@ -13,6 +13,7 @@ package org.openhab.binding.nikohomecontrol.internal.protocol; import java.time.LocalDateTime; +import java.util.Map; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -33,7 +34,7 @@ public interface NhcMeterEvent extends NhcBaseEvent { * * @param power current power consumption/production in W (positive for consumption), null for an empty reading */ - void meterPowerEvent(@Nullable Integer power); + void meterPowerEvent(@Nullable Double power); /** * This method is called when a meter event is received from the Niko Home Control controller and separate @@ -43,9 +44,8 @@ public interface NhcMeterEvent extends NhcBaseEvent { * @param powerFromGrid current power consumption from grid in W, null for an empty reading * @param powerToGrid current power sent to grid in W, null for an empty reading */ - default void meterPowerEvent(@Nullable Integer power, @Nullable Integer powerFromGrid, - @Nullable Integer powerToGrid) { - meterPowerEvent(power); + default void meterPowerEvent(@Nullable Double power, @Nullable Double powerFromGrid, @Nullable Double powerToGrid) { + meterPowerEvent(power, powerFromGrid, powerToGrid); } /** @@ -53,7 +53,7 @@ default void meterPowerEvent(@Nullable Integer power, @Nullable Integer powerFro * * @param peakPowerFromGrid current month peak power from grid */ - default void meterPeakPowerFromGridEvent(int peakPowerFromGrid) { + default void meterPeakPowerFromGridEvent(double peakPowerFromGrid) { } /** @@ -64,4 +64,15 @@ default void meterPeakPowerFromGridEvent(int peakPowerFromGrid) { * @param lastReadingUTC last meter reading date and time, UTC */ void meterReadingEvent(double reading, double dayReading, LocalDateTime lastReadingUTC); + + /** + * This method is called when meter readinsg are received from the Niko Home Control controller. + * This method should be used for meters that register multiple measurements at the same time. + * The keys of the argument maps are the keys to the readings as received from the controller. + * + * @param readings meter readings + * @param dayReadings meter readings for current day + * @param lastReadingUTC last meter reading date and time, UTC + */ + void meterReadingEvent(Map readings, Map dayReadings, LocalDateTime lastReadingUTC); } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NikoHomeControlCommunication.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NikoHomeControlCommunication.java index 9ab87b9d1313e..444015c59fb99 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NikoHomeControlCommunication.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NikoHomeControlCommunication.java @@ -244,8 +244,9 @@ public Map getCarChargerDevices() { * callback in {@link NhcMeterEvent}. * * @param meterId + * @param startDate 0 reference for the meter */ - public abstract void executeMeter(String meterId); + public abstract void executeMeter(String meterId, String startDate); /** * Start retrieving energy meter data from Niko Home Control. The method is used to regularly retrigger the @@ -287,10 +288,10 @@ public void stopMeterLive(String meterId) { * @param meterId * @param refresh reading frequency in minutes */ - public void startMeter(String meterId, int refresh) { + public void startMeter(String meterId, int refresh, String startDate) { NhcMeter meter = getMeters().get(meterId); if (meter != null) { - meter.startMeter(refresh); + meter.startMeter(refresh, startDate); } } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NikoHomeControlConstants.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NikoHomeControlConstants.java index d8fce8b415334..b4e06e4961247 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NikoHomeControlConstants.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NikoHomeControlConstants.java @@ -44,12 +44,16 @@ public static enum AccessType { // Meter types abstracted from NhcI and NhcII meter types public static enum MeterType { ENERGY_LIVE, + ENERGY_HOME, ENERGY, GAS, WATER, GENERIC } + // NhcII measurements base URL + public static final String NHC_MEASUREMENTS_BASEURL = "/measurements/v1/devices"; + // switch and dimmer constants in the Nhc layer public static final String NHCON = "On"; public static final String NHCOFF = "Off"; @@ -122,4 +126,13 @@ public static enum MeterType { NHCNOCREDENTIALS, "NO CREDENTIALS", NHCINVALIDCREDENTIALS, "INVALID CREDENTIALS", NHCCONNECTIONERROR, "CONNECTION ERROR", NHCCONNECTIONTIMEOUT, "CONNECTION TIMEOUT", NHCAPIERROR, "API ERROR", NHCUNKNOWNERROR, "UNKNOWN ERROR"); + + // NhcII energy channels + public static final String NHC_ELECTRICAL_ENERGY = "electricalEnergy"; + public static final String NHC_ELECTRICAL_ENERGY_CONSUMPTION = "electricalEnergyConsumption"; + public static final String NHC_ELECTRICAL_ENERGY_TO_GRID = "electricalEnergyToGrid"; + public static final String NHC_ELECTRICAL_ENERGY_FROM_GRID = "electricalEnergyFromGrid"; + public static final String NHC_ELECTRICAL_ENERGY_SELF_CONSUMPTION = "electricalEnergySelfConsumption"; + public static final String NHC_GAS_VOLUME = "gasVolume"; + public static final String NHC_WATER_VOLUME = "waterVolume"; } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NhcMeter1.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NhcMeter1.java index 38be345c1640d..69dd3db1f7c27 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NhcMeter1.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NhcMeter1.java @@ -35,6 +35,9 @@ public class NhcMeter1 extends NhcMeter { private final String meterType; + protected volatile double reading; + protected volatile double dayReading; + NhcMeter1(String id, String name, MeterType meterType, @Nullable String location, String type, @Nullable LocalDateTime referenceDate, NikoHomeControlCommunication nhcComm, ScheduledExecutorService scheduler) { @@ -49,4 +52,38 @@ public class NhcMeter1 extends NhcMeter { public String getMeterType() { return meterType; } + + @Override + public double getReading() { + // For energy, readings are in W per 10 min, convert to kWh + // For water and gas, readings are in 0.1 dm^3, convert to m^3 + return ((type == MeterType.ENERGY) || (type == MeterType.ENERGY_LIVE)) ? (reading / 6000.0) + : (reading / 10000.0); + } + + @Override + public double getDayReading() { + // For energy, readings are in W per 10 min, convert to kWh + // For water and gas, readings are in 0.1 dm^3, convert to m^3 + return ((type == MeterType.ENERGY) || (type == MeterType.ENERGY_LIVE)) ? (dayReading / 6000.0) + : (dayReading / 10000.0); + } + + @Override + public double getReadingRaw() { + return reading; + } + + @Override + public double getDayReadingRaw() { + return dayReading; + } + + @Override + public void setReading(double reading, double dayReading, LocalDateTime lastReading) { + this.reading = reading; + this.dayReading = dayReading; + this.lastReadingUTC = lastReading; + updateReadingState(); + } } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NikoHomeControlCommunication1.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NikoHomeControlCommunication1.java index 4eb13f2202e5e..cf8e82f997ffb 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NikoHomeControlCommunication1.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc1/NikoHomeControlCommunication1.java @@ -29,7 +29,6 @@ import java.time.temporal.ChronoUnit; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; @@ -563,7 +562,7 @@ private void cmdListActions(List> data) { logger.debug("name not found in action {}", action); continue; } - String type = Optional.ofNullable(action.get("type")).orElse(""); + String type = action.getOrDefault("type", ""); ActionType actionType = ActionType.GENERIC; switch (type) { case "0": @@ -804,13 +803,13 @@ private void cmdGetEnergyData(List data, String id, @Nullable LocalDateT dayReading = data.stream().skip(beforeDayStart).mapToInt(Integer::parseInt).sum(); } else { int value = data.stream().skip(1).mapToInt(Integer::parseInt).sum(); - reading = meter.getReadingInt() + value; + reading = (int) meter.getReadingRaw() + value; logger.trace("adding {} to meter {} reading, new reading {}", value, id, reading); if (dayChange) { dayReading = data.stream().skip(1 + beforeDayStart).mapToInt(Integer::parseInt).sum(); logger.trace("meter {} day reading, it's a new day, new reading {}", id, dayReading); } else { - dayReading = meter.getDayReadingInt() + value; + dayReading = (int) meter.getDayReadingRaw() + value; logger.trace("adding {} to meter {} day reading, new reading {}", value, id, dayReading); } } @@ -886,7 +885,7 @@ private void eventGetLive(Map data) { logger.debug("event live power channel {} with v {}", channel, v); NhcMeter e = getMeters().get(channel); if (e != null) { - e.setPower(v); + e.setPower(Double.valueOf(v)); } } catch (IllegalArgumentException e) { // do nothing @@ -945,7 +944,7 @@ public void executeThermostat(String thermostatId, int overruleTemp, int overrul } @Override - public void executeMeter(String meterId) { + public void executeMeter(String meterId, String startDate) { NhcMeter meter = getMeters().get(meterId); if (meter == null) { return; @@ -987,7 +986,13 @@ public void executeMeter(String meterId) { LocalDateTime start = meter.getLastReading(); if (start == null) { meterReadingInit = true; - start = meter.getReferenceDate(); + try { + start = LocalDateTime.parse(startDate); + } catch (DateTimeParseException e) { + start = meter.getReferenceDate(); + logger.debug("not able to parse meter start date {}, default to reference date {}", startDate, + start); + } if (start == null) { logger.debug("error getting meter data, no meter reference date available"); return; diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcDevice2.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcDevice2.java index bb939f8986344..f54f98c840098 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcDevice2.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcDevice2.java @@ -77,8 +77,6 @@ static class NhcProperty { // fields for electricity metering @Nullable - String electricalEnergy; - @Nullable String electricalPower; @Nullable String electricalPowerToGrid; @@ -96,6 +94,20 @@ static class NhcProperty { String electricalPowerProductionThresholdExceeded; @Nullable String reportInstantUsage; + @Nullable + String electricalEnergy; + @Nullable + String electricalEnergyConsumption; + @Nullable + String electricalEnergyToGrid; + @Nullable + String electricalEnergyFromGrid; + @Nullable + String electricalEnergySelfConsumption; + @Nullable + String gasVolume; + @Nullable + String waterVolume; // fields for access control @Nullable diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcHttpConnection2.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcHttpConnection2.java new file mode 100644 index 0000000000000..8488464533492 --- /dev/null +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcHttpConnection2.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2010-2025 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.nikohomecontrol.internal.protocol.nhc2; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; +import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; + +/** + * {@link NhcHttpConnection2} manages the HTTP connection to the Connected Controller. This connection is used to + * retrieve measurements. + * + * @author Mark Herwege - Initial Contribution + */ +@NonNullByDefault +public class NhcHttpConnection2 { + + private final Logger logger = LoggerFactory.getLogger(NhcHttpConnection2.class); + + private final HttpClient httpClient; + private String token; + private String hostname; + private String measurementsBaseUrl; + + private static final Gson GSON = new Gson(); + + private static final long TIMEOUT_MS = 1000; + + NhcHttpConnection2(HttpClient httpClient, String cocoAddress, String token) { + this.httpClient = httpClient; + this.token = token; + this.hostname = cocoAddress; + measurementsBaseUrl = "https://" + cocoAddress + NikoHomeControlConstants.NHC_MEASUREMENTS_BASEURL; + } + + public @Nullable String getMeasurements(String deviceUuid, LocalDateTime start, LocalDateTime end) { + String intervalStart = start.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); + String intervalEnd = end.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); + String url = measurementsBaseUrl + "/" + deviceUuid + "/total?Aggregation=sum&IntervalStart=" + intervalStart + + "&IntervalEnd=" + intervalEnd; + logger.trace("Get Total Measurements: {}", url); + Request request = httpClient.newRequest(url); + ContentResponse response; + try { + response = request.method(HttpMethod.GET).header(HttpHeader.AUTHORIZATION, "Bearer " + token) + .header(HttpHeader.HOST, hostname).header(HttpHeader.ACCEPT, "application/json") + .timeout(TIMEOUT_MS, TimeUnit.MILLISECONDS).send(); + } catch (InterruptedException | TimeoutException | ExecutionException e) { + logger.debug("Error with query {}", url); + return null; + } + + return response(url, response); + } + + private @Nullable String response(String url, ContentResponse response) { + int status = response.getStatus(); + String responseString = response.getContentAsString(); + if (status == 200) { + logger.debug("Query {}, response: {}", url, responseString); + return responseString; + } + + JsonObject jsonObject = GSON.fromJson(responseString, JsonObject.class); + String message = null; + String messageDetail = null; + if (jsonObject != null) { + if (jsonObject.has("Message") && !jsonObject.get("Message").isJsonNull()) { + message = jsonObject.get("Message").getAsString(); + } + if (jsonObject.has("MessageDetail") && !jsonObject.get("MessageDetail").isJsonNull()) { + messageDetail = jsonObject.get("MessageDetail").getAsString(); + } + } + logger.warn("Error query {}: status {}, message {}, detail {}", url, status, message, messageDetail); + return null; + } +} diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcMeter2.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcMeter2.java index 565caa8a4e480..5b956b8d3bfe2 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcMeter2.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcMeter2.java @@ -13,13 +13,19 @@ package org.openhab.binding.nikohomecontrol.internal.protocol.nhc2; import java.time.LocalDateTime; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledExecutorService; +import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.nikohomecontrol.internal.protocol.NhcMeter; +import org.openhab.binding.nikohomecontrol.internal.protocol.NhcMeterEvent; import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlCommunication; import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlConstants.MeterType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The {@link NhcMeter2} class represents the meter Niko Home Control communication object. It contains all fields @@ -30,10 +36,20 @@ @NonNullByDefault public class NhcMeter2 extends NhcMeter { + private final Logger logger = LoggerFactory.getLogger(NhcMeter2.class); + private final String deviceType; private final String deviceTechnology; private final String deviceModel; + protected Map readings = new ConcurrentHashMap<>(); + protected Map dayReadings = new ConcurrentHashMap<>(); + + protected volatile double fromGridReading; + protected volatile double fromGridDayReading; + protected volatile double toGridReading; + protected volatile double toGridDayReading; + protected NhcMeter2(String id, String name, MeterType meterType, String deviceType, String deviceTechnology, String deviceModel, @Nullable LocalDateTime referenceDate, @Nullable String location, NikoHomeControlCommunication nhcComm, ScheduledExecutorService scheduler) { @@ -63,4 +79,44 @@ public String getDeviceTechnology() { public String getDeviceModel() { return deviceModel; } + + @Override + public Map getReadings() { + return readings; + } + + @Override + public Map getDayReadings() { + return dayReadings; + } + + @Override + public void setReadings(Map readings, Map dayReadings, LocalDateTime lastReading) { + if (readings.isEmpty() || dayReadings.isEmpty()) { + return; + } + this.lastReadingUTC = lastReading; + updateReadingState(); + } + + @Override + protected void updateReadingState() { + NhcMeterEvent handler = eventHandler; + Map readings = getReadings(); + Map dayReadings = getDayReadings(); + LocalDateTime lastReading = getLastReading(); + if ((handler != null) && (lastReading != null)) { + if (logger.isDebugEnabled()) { + String readingsString = "[" + readings.entrySet().stream() + .map(e -> String.format("%s: %s", e.getKey(), e.getValue())).collect(Collectors.joining(", ")) + + "]"; + String dayReadingsString = "[" + dayReadings.entrySet().stream() + .map(e -> String.format("%s: %s", e.getKey(), e.getValue())).collect(Collectors.joining(", ")) + + "]"; + logger.debug("update meter reading channels for {} with {}, day {}", id, readingsString, + dayReadingsString); + } + handler.meterReadingEvent(readings, dayReadings, lastReading); + } + } } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcMeterReading2.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcMeterReading2.java new file mode 100644 index 0000000000000..e3bf777139db6 --- /dev/null +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcMeterReading2.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2010-2025 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.nikohomecontrol.internal.protocol.nhc2; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * {@link NhcMeterReading2} represents a Niko Home Control II meter readings. It is used when parsing meter reading + * responses from http meter reading requests. + * + * @author Mark Herwege - Initial Contribution + */ +@NonNullByDefault +class NhcMeterReading2 { + static class NhcProperty { + @Nullable + String property; + @Nullable + String unit; + @Nullable + List values; + } + + static class NhcMeterValue { + @Nullable + String dateTime; + double value; + } + + String deviceUuid = ""; + @Nullable + List properties; +} diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NikoHomeControlCommunication2.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NikoHomeControlCommunication2.java index f2378895de004..67e7424548296 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NikoHomeControlCommunication2.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NikoHomeControlCommunication2.java @@ -17,8 +17,13 @@ import java.lang.reflect.Type; import java.net.InetAddress; import java.security.cert.CertificateException; +import java.time.LocalDateTime; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; @@ -31,8 +36,12 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; +import javax.measure.Unit; +import javax.measure.quantity.Energy; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; import org.openhab.binding.nikohomecontrol.internal.protocol.NhcAccess; import org.openhab.binding.nikohomecontrol.internal.protocol.NhcAction; import org.openhab.binding.nikohomecontrol.internal.protocol.NhcAlarm; @@ -49,10 +58,13 @@ import org.openhab.binding.nikohomecontrol.internal.protocol.nhc2.NhcDevice2.NhcProperty; import org.openhab.binding.nikohomecontrol.internal.protocol.nhc2.NhcDevice2.NhcTrait; import org.openhab.binding.nikohomecontrol.internal.protocol.nhc2.NhcMessage2.NhcMessageParam; +import org.openhab.binding.nikohomecontrol.internal.protocol.nhc2.NhcMeterReading2.NhcMeterValue; import org.openhab.core.io.transport.mqtt.MqttConnectionObserver; import org.openhab.core.io.transport.mqtt.MqttConnectionState; import org.openhab.core.io.transport.mqtt.MqttException; import org.openhab.core.io.transport.mqtt.MqttMessageSubscriber; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -83,6 +95,8 @@ public class NikoHomeControlCommunication2 extends NikoHomeControlCommunication private final Logger logger = LoggerFactory.getLogger(NikoHomeControlCommunication2.class); private final NhcMqttConnection2 mqttConnection; + private final HttpClient httpClient; + private @Nullable NhcHttpConnection2 httpConnection; private final List services = new CopyOnWriteArrayList<>(); @@ -107,9 +121,10 @@ public class NikoHomeControlCommunication2 extends NikoHomeControlCommunication * */ public NikoHomeControlCommunication2(NhcControllerEvent handler, String clientId, - ScheduledExecutorService scheduler) throws CertificateException { + ScheduledExecutorService scheduler, HttpClient httpClient) throws CertificateException { super(handler, scheduler); - mqttConnection = new NhcMqttConnection2(clientId, this, this); + this.mqttConnection = new NhcMqttConnection2(clientId, this, this); + this.httpClient = httpClient; } @Override @@ -136,6 +151,8 @@ public synchronized void startCommunication() { return; } + httpConnection = new NhcHttpConnection2(httpClient, addrString, token); + try { mqttConnection.startConnection(addrString, port, profile, token); } catch (MqttException e) { @@ -378,6 +395,10 @@ private void notificationEvt(String response) { } private void addDevice(NhcDevice2 device) { + List properties = device.properties; + if (properties == null || properties.isEmpty()) { + return; + } String location = null; List parameters = device.parameters; if (parameters != null) { @@ -388,14 +409,14 @@ private void addDevice(NhcDevice2 device) { addVideoDevice(device); } else if ("accesscontrol".equals(device.model) || "bellbutton".equals(device.model)) { addAccessDevice(device, location); - } else if ("alarms".equals(device.model) && (device.properties != null) - && (device.properties.stream().anyMatch(p -> (p.alarmActive != null)))) { + } else if ("alarms".equals(device.model) && (properties.stream().anyMatch(p -> (p.alarmActive != null)))) { addAlarmDevice(device, location); } else if ("action".equals(device.type) || "relay".equals(device.type) || "virtual".equals(device.type)) { addActionDevice(device, location); } else if ("thermostat".equals(device.type) || "hvac".equals(device.type)) { addThermostatDevice(device, location); - } else if ("centralmeter".equals(device.type) || "energyhome".equals(device.type)) { + } else if ("centralmeter".equals(device.type) || "energyhome".equals(device.type) + || "smartplug".equals(device.type)) { addMeterDevice(device, location); } else if ("chargingstation".equals(device.type)) { addCarChargerDevice(device, location); @@ -435,6 +456,8 @@ private void addActionDevice(NhcDevice2 device, @Nullable String location) { case "sunblind": case "venetianblind": case "gate": + case "reynaers": + case "velux": actionType = ActionType.ROLLERSHUTTER; break; default: @@ -477,8 +500,19 @@ private void addMeterDevice(NhcDevice2 device, @Nullable String location) { nhcMeter.setLocation(location); } else { logger.debug("adding energy meter device {} model {}, {}", device.uuid, device.model, device.name); - nhcMeter = new NhcMeter2(device.uuid, device.name, MeterType.ENERGY_LIVE, device.type, device.technology, - device.model, null, location, this, scheduler); + MeterType meterType; + if ("energyhome".equals(device.type)) { + meterType = MeterType.ENERGY_HOME; + } else { + meterType = switch (device.model) { + case "electricity-pulse" -> MeterType.ENERGY; + case "gas" -> MeterType.GAS; + case "water" -> MeterType.WATER; + default -> MeterType.ENERGY_LIVE; + }; + } + nhcMeter = new NhcMeter2(device.uuid, device.name, meterType, device.type, device.technology, device.model, + null, location, this, scheduler); } meters.put(device.uuid, nhcMeter); } @@ -728,22 +762,22 @@ private void updateRollershutterState(NhcAction2 action, List devic } private void updateThermostatState(NhcThermostat2 thermostat, List deviceProperties) { - Optional overruleActiveProperty = deviceProperties.stream().map(p -> p.overruleActive) - .filter(Objects::nonNull).map(t -> Boolean.parseBoolean(t)).findFirst(); - Optional overruleSetpointProperty = deviceProperties.stream().map(p -> p.overruleSetpoint) + Boolean overruleActiveProperty = deviceProperties.stream().map(p -> p.overruleActive).filter(Objects::nonNull) + .map(t -> Boolean.parseBoolean(t)).findFirst().orElse(null); + Integer overruleSetpointProperty = deviceProperties.stream().map(p -> p.overruleSetpoint) .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s) * 10) : null) - .filter(Objects::nonNull).findFirst(); - Optional overruleTimeProperty = deviceProperties.stream().map(p -> p.overruleTime) + .filter(Objects::nonNull).findFirst().orElse(null); + Integer overruleTimeProperty = deviceProperties.stream().map(p -> p.overruleTime) .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null) - .filter(Objects::nonNull).findFirst(); - Optional setpointTemperatureProperty = deviceProperties.stream().map(p -> p.setpointTemperature) + .filter(Objects::nonNull).findFirst().orElse(null); + Integer setpointTemperatureProperty = deviceProperties.stream().map(p -> p.setpointTemperature) .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s) * 10) : null) - .filter(Objects::nonNull).findFirst(); - Optional ecoSaveProperty = deviceProperties.stream().map(p -> p.ecoSave) - .map(s -> s != null ? Boolean.parseBoolean(s) : null).filter(Objects::nonNull).findFirst(); - Optional ambientTemperatureProperty = deviceProperties.stream().map(p -> p.ambientTemperature) + .filter(Objects::nonNull).findFirst().orElse(null); + Boolean ecoSaveProperty = deviceProperties.stream().map(p -> p.ecoSave) + .map(s -> s != null ? Boolean.parseBoolean(s) : null).filter(Objects::nonNull).findFirst().orElse(null); + Integer ambientTemperatureProperty = deviceProperties.stream().map(p -> p.ambientTemperature) .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s) * 10) : null) - .filter(Objects::nonNull).findFirst(); + .filter(Objects::nonNull).findFirst().orElse(null); Optional demandProperty = deviceProperties.stream().map(p -> p.demand).filter(Objects::nonNull) .findFirst(); Optional operationModeProperty = deviceProperties.stream().map(p -> p.operationMode) @@ -754,18 +788,18 @@ private void updateThermostatState(NhcThermostat2 thermostat, List int mode = IntStream.range(0, THERMOSTATMODES.length).filter(i -> THERMOSTATMODES[i].equals(modeString)) .findFirst().orElse(thermostat.getMode()); - int measured = ambientTemperatureProperty.orElse(thermostat.getMeasured()); - int setpoint = setpointTemperatureProperty.orElse(thermostat.getSetpoint()); + int measured = ambientTemperatureProperty != null ? ambientTemperatureProperty : thermostat.getMeasured(); + int setpoint = setpointTemperatureProperty != null ? setpointTemperatureProperty : thermostat.getSetpoint(); int overrule = 0; int overruletime = 0; - if (overruleActiveProperty.orElse(true)) { - overrule = overruleSetpointProperty.orElse(thermostat.getOverrule()); - overruletime = overruleTimeProperty.orElse(thermostat.getRemainingOverruletime()); + if (overruleActiveProperty == null || overruleActiveProperty) { + overrule = overruleSetpointProperty != null ? overruleSetpointProperty : thermostat.getOverrule(); + overruletime = overruleTimeProperty != null ? overruleTimeProperty : thermostat.getRemainingOverruletime(); } int ecosave = thermostat.getEcosave(); - if (ecoSaveProperty.orElse(false)) { + if (ecoSaveProperty != null && ecoSaveProperty) { ecosave = 1; } @@ -792,34 +826,38 @@ private void updateThermostatState(NhcThermostat2 thermostat, List private void updateMeterState(NhcMeter2 meter, List deviceProperties) { try { - Optional electricalPower = deviceProperties.stream().map(p -> p.electricalPower) - .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null) - .filter(Objects::nonNull).findFirst(); - Optional powerFromGrid = deviceProperties.stream().map(p -> p.electricalPowerFromGrid) - .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null) - .filter(Objects::nonNull).findFirst(); - Optional powerToGrid = deviceProperties.stream().map(p -> p.electricalPowerToGrid) - .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null) - .filter(Objects::nonNull).findFirst(); - int power = electricalPower.orElse(powerFromGrid.orElse(0) - powerToGrid.orElse(0)); + Optional electricalPower = deviceProperties.stream().map(p -> p.electricalPower) + .map(s -> (!((s == null) || s.isEmpty())) ? Double.parseDouble(s) : null).filter(Objects::nonNull) + .findFirst(); + @SuppressWarnings("null") + double powerFromGrid = deviceProperties.stream().map(p -> p.electricalPowerFromGrid) + .map(s -> (!((s == null) || s.isEmpty())) ? Double.parseDouble(s) : null).filter(Objects::nonNull) + .findFirst().orElse(0.0); + @SuppressWarnings("null") + double powerToGrid = deviceProperties.stream().map(p -> p.electricalPowerToGrid) + .map(s -> (!((s == null) || s.isEmpty())) ? Double.parseDouble(s) : null).filter(Objects::nonNull) + .findFirst().orElse(0.0); + @SuppressWarnings("null") + double power = electricalPower.orElse(powerFromGrid - powerToGrid); logger.trace("setting energy meter {} power to {}, powerFromGrid to {}, powerToGrid to {}", meter.getId(), - power, powerFromGrid.orElse(null), powerToGrid.orElse(null)); - meter.setPower(power, powerFromGrid.orElse(null), powerToGrid.orElse(null)); + power, powerFromGrid, powerToGrid); + meter.setPower(power, powerFromGrid, powerToGrid); } catch (NumberFormatException e) { logger.trace("wrong format in energy meter {} power reading", meter.getId()); meter.setPower(null, null, null); } try { - Integer peakPowerFromGrid = deviceProperties.stream().map(p -> p.electricalMonthlyPeakPowerFromGrid) - .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null) - .filter(Objects::nonNull).findFirst().orElse(null); + Double peakPowerFromGrid = deviceProperties.stream().map(p -> p.electricalMonthlyPeakPowerFromGrid) + .map(s -> (!((s == null) || s.isEmpty())) ? Double.parseDouble(s) : null).filter(Objects::nonNull) + .findFirst().orElse(null); if (peakPowerFromGrid != null) { - logger.trace("setting energy meter {} peakPowerFromGrid to {}", peakPowerFromGrid); + logger.trace("setting energy meter {} peakPowerFromGrid to {}", meter.getId(), peakPowerFromGrid); meter.setPeakPowerFromGrid(peakPowerFromGrid); } } catch (NumberFormatException e) { logger.trace("wrong format in energy meter {} peakPowerFromGrid reading", meter.getId()); + meter.setPeakPowerFromGrid(null); } } @@ -1077,8 +1115,75 @@ public void executeThermostat(String thermostatId, int overruleTemp, int overrul } @Override - public void executeMeter(String meterId) { - // Nothing to do, individual meter readings not supported in NHC II at this point in time + public void executeMeter(String meterId, String startDate) { + NhcMeter meter = meters.get(meterId); + if (meter == null) { + return; + } + + NhcHttpConnection2 httpConnection = this.httpConnection; + if (httpConnection == null) { + return; + } + LocalDateTime now = ZonedDateTime.now().withZoneSameInstant(getTimeZone()).toLocalDateTime(); + LocalDateTime dayStart = now.truncatedTo(ChronoUnit.DAYS); + LocalDateTime meterStart = meter.getLastReading(); + if (meterStart == null) { + meterStart = LocalDateTime.parse(startDate); + } + + String response = httpConnection.getMeasurements(meterId, meterStart, now); + logger.trace("Meter measurement: {}", response); + final Map readings = response != null ? parseMeterReadings(response) : Map.of(); + + response = httpConnection.getMeasurements(meterId, dayStart, now); + logger.trace("Day measurement: {}", response); + final Map dayReadings = response != null ? parseMeterReadings(response) : Map.of(); + + meter.setReadings(readings, dayReadings, now); + } + + private Map parseMeterReadings(String response) { + Map readings = new HashMap<>(); + + Type messageType = new TypeToken() { + }.getType(); + try { + NhcMeterReading2 message = gson.fromJson(response, messageType); + List meterReadingList = message != null ? message.properties : null; + if (meterReadingList == null) { + return Map.of(); + } + meterReadingList.forEach(r -> { + String property = r.property; + String unit = r.unit; + List values = r.values; + if (property != null && values != null && !values.isEmpty()) { + double value = values.getFirst().value; + if (unit != null) { + try { + Unit receivedUnit = Units.getInstance().getUnit(unit).asType(Energy.class); + Unit targetUnit = Units.getInstance().getUnit("kWh").asType(Energy.class); + if (receivedUnit != null && targetUnit != null) { + QuantityType quantityValue = QuantityType.valueOf(value, receivedUnit) + .toUnit(targetUnit); + if (quantityValue != null) { + value = quantityValue.doubleValue(); + } + } + } catch (ClassCastException e) { + logger.debug("Unit conversion failed for unit {}: {}", unit, e.getMessage()); + } + } + readings.put(property, value); + } + }); + + } catch (JsonSyntaxException e) { + logger.debug("unexpected json {}", response); + } + + return readings; } @Override diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/i18n/nikohomecontrol.properties b/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/i18n/nikohomecontrol.properties index 6e9d1d4b580a2..e2562fff73113 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/i18n/nikohomecontrol.properties +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/i18n/nikohomecontrol.properties @@ -46,6 +46,9 @@ thermostatDescription = Thermostat in the Niko Home Control system energyMeterLiveLabel = Energy Meter Live energyMeterLiveDescription = Energy meter with live power consumption in the Niko Home Control system +energyMeterHomeLabel = Energy Meter Home +energyMeterHomeDescription = Home energy meter in the Niko Home Control system, includes power from grid and power to grid + energyMeterLabel = Energy Meter energyMeterDescription = Energy meter in the Niko Home Control system @@ -87,6 +90,10 @@ meterConfigRefreshLabel = Refresh Interval meterConfigRefreshDescription = Refresh interval for meter reading in minutes, default 10 minutes. The value should not be lower than 5 minutes to \ avoid too many meter data retrieval calls +meterConfigStartDateLabel = Start Date +meterConfigStartDateDescription = Start date for the meter, the meter will start at 0 at this date, by default will be the reference date for NHC I \ + and the creation date of the meter thing for NHC II + #channel types channelButtonLabel = Button channelButtonDescription = Pushbutton control for action in Niko Home Control @@ -131,9 +138,21 @@ channelPeakPowerFromGridDescription = Peak power from grid in current month, onl channelEnergyLabel = Energy channelEnergyDescription = Energy consumption/production (positive is consumption) +channelEnergyFromGridLabel = Energy From Grid +channelEnergyFromGridDescription = Energy consumed from grid +channelEnergyToGridLabel = Energy To Grid +channelEnergyToGridDescription = Energy send to grid +channelEnergySelfConsumptionLabel = Energy Self Consumption +channelEnergySelfConsumptionDescription = Energy produced and consumed without sending to grid channelEnergyDayLabel = Energy Today channelEnergyDayDescription = Today's energy consumption/production (positive is consumption) +channelEnergyFromGridDayLabel = Energy From Grid Today +channelEnergyFromGridDayDescription = Today's energy consumed from grid +channelEnergyToGridDayLabel = Energy To Grid Today +channelEnergyToGridDayDescription = Today's energy send to grid +channelEnergySelfConsumptionDayLabel = Energy Self Consumption Today +channelEnergySelfConsumptionDayDescription = Today's energy produced and consumed without sending to grid channelGasLabel = Gas channelGasDescription = Gas consumption @@ -227,6 +246,8 @@ offline.configuration-error.deviceRemoved = Device has been removed from control offline.configuration-error.actionType = Unsupported action type offline.configuration-error.meterType = Unsupported meter type +offline.configration-err.meterStartDate = Cannot parse meter start date configuration parameter + offline.configuration-error.invalid-bridge-handler = Invalid bridge handler offline.bridge-unitialized = Bridge unitialized diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml index 7adf5b203b9bf..5ea5acd8063bf 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml @@ -186,30 +186,53 @@ - + - - - @text/energyMeterLiveDescription + + @text/energyMeterHomeDescription ElectricMeter - + @text/channelPowerFromGridDescription - + @text/channelPowerToGridDescription - + @text/channelPeakPowerFromGridDescription + + + @text/channelEnergyFromGridDescription + + + + @text/channelEnergyFromGridDescription + + + + @text/channelEnergyFromGridDayDescription + + + + @text/channelEnergyToGridDescription + + + + @text/channelEnergySelfConsumptionDescription + + + + @text/channelEnergySelfConsumptionDayDescription + @@ -223,6 +246,45 @@ 10 true + + datetime + + @text/meterConfigStartDateDescription + true + + + + + + + + + + @text/energyMeterLiveDescription + ElectricMeter + + + + + + + + + + @text/deviceConfigDeviceIdDescription + + + + @text/meterConfigRefreshDescription + 10 + true + + + datetime + + @text/meterConfigStartDateDescription + true + @text/meterConfigInvertDescription @@ -234,6 +296,7 @@ + @text/energyMeterDescription @@ -254,6 +317,12 @@ 10 true + + datetime + + @text/meterConfigStartDateDescription + true + @text/meterConfigInvertDescription @@ -265,6 +334,7 @@ + @text/gasMeterDescription @@ -296,6 +366,7 @@ + @text/waterMeterDescription @@ -528,16 +599,6 @@ - - Number:Power - - @text/channelPowerDescription - - Measurement - Power - - - Number:Energy From bd450321737e302aed1bcbd1ef40bbf277bacde7 Mon Sep 17 00:00:00 2001 From: Mark Herwege Date: Fri, 10 Oct 2025 16:52:20 +0200 Subject: [PATCH 04/13] add gas and water metering to NHCII Signed-off-by: Mark Herwege --- .../README.md | 8 +- .../NikoHomeControlBindingConstants.java | 4 +- .../handler/NikoHomeControlMeterHandler.java | 110 +++++++++--------- .../internal/protocol/nhc2/NhcMeter2.java | 5 - .../nhc2/NikoHomeControlCommunication2.java | 17 ++- .../resources/OH-INF/thing/thing-types.xml | 38 +++++- .../main/resources/OH-INF/update/update.xml | 39 +++++++ 7 files changed, 142 insertions(+), 79 deletions(-) create mode 100644 bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/update/update.xml diff --git a/bundles/org.openhab.binding.nikohomecontrol/README.md b/bundles/org.openhab.binding.nikohomecontrol/README.md index 9611891fbb8e0..646524d5cf1bf 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/README.md +++ b/bundles/org.openhab.binding.nikohomecontrol/README.md @@ -214,10 +214,10 @@ Thing nikohomecontrol:carCharger:mybridge:mycarcharger [ carChargerId="abcdef01- | peakPowerFromGrid | R | | Number:Power | energyMeterHome | current month peak power as registered by the home energy meter | | energy | R | | Number:Energy | energyMeterHome, energyMeterLive, energyMeter | total energy meter reading | | energyday | R | | Number:Energy | energyMeterHome, energyMeterLive, energyMeter | day energy meter reading | -| gas | R | | Number:Volume | gasMeter | total gas meter reading | -| gasday | R | | Number:Volume | gasMeter | day gas meter reading | -| water | R | | Number:Volume | waterMeter | total water meter reading | -| waterday | R | | Number:Volume | waterMeter | day water meter reading | +| gas | R | | Number:Volume | energyMeterHome, gasMeter | total gas meter reading | +| gasday | R | | Number:Volume | energyMeterHome, gasMeter | day gas meter reading | +| water | R | | Number:Volume | energyMeterHome, waterMeter | total water meter reading | +| waterday | R | | Number:Volume | energyMeterHome, waterMeter | day water meter reading | | measurementtime | R | | DateTimeType | energyMeterHome, energyMeterLive, energyMeter, gasMeter, waterMeter | last meter reading time | | bellbutton | RW | | Switch | access, accessRingAndComeIn | bell button connected to access device, including buttons on video phone devices linked to an access device. The bell can also be triggered by an `ON` command, `autoupdate="false"` by default | | ringandcomein | RW | | Switch | accessRingAndComeIn | provide state and turn automatic door unlocking at bell ring on/off | diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/NikoHomeControlBindingConstants.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/NikoHomeControlBindingConstants.java index 423df54d3470c..542f7330cb222 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/NikoHomeControlBindingConstants.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/NikoHomeControlBindingConstants.java @@ -102,9 +102,7 @@ public class NikoHomeControlBindingConstants { public static final String CHANNEL_ENERGY_FROM_GRID_DAY = "energyfromgridday"; public static final String CHANNEL_ENERGY_TO_GRID_DAY = "energytogridday"; public static final String CHANNEL_ENERGY_SELF_CONSUMPTION_DAY = "energyselfconsumptionday"; - public static final String CHANNEL_ENERGY_LAST = "energylast"; - public static final String CHANNEL_GAS_LAST = "gaslast"; - public static final String CHANNEL_WATER_LAST = "waterlast"; + public static final String CHANNEL_MEASUREMENT_TIME = "measurementtime"; public static final String CHANNEL_BELL_BUTTON = "bellbutton"; public static final String CHANNEL_RING_AND_COME_IN = "ringandcomein"; diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlMeterHandler.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlMeterHandler.java index ca9697e174646..9caa10cf70a4c 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlMeterHandler.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlMeterHandler.java @@ -92,9 +92,7 @@ void handleCommandSelection(ChannelUID channelUID, Command command) { case CHANNEL_ENERGY_DAY: case CHANNEL_GAS_DAY: case CHANNEL_WATER_DAY: - case CHANNEL_ENERGY_LAST: - case CHANNEL_GAS_LAST: - case CHANNEL_WATER_LAST: + case CHANNEL_MEASUREMENT_TIME: LocalDateTime lastReadingUTC = nhcMeter.getLastReading(); if (lastReadingUTC != null) { meterReadingEvent(nhcMeter.getReading(), nhcMeter.getDayReading(), lastReadingUTC); @@ -326,19 +324,19 @@ public void meterReadingEvent(double meterReading, double meterReadingDay, Local case ENERGY: updateState(CHANNEL_ENERGY, new QuantityType<>(value, KILOWATT_HOUR)); updateState(CHANNEL_ENERGY_DAY, new QuantityType<>(dayValue, KILOWATT_HOUR)); - updateState(CHANNEL_ENERGY_LAST, new DateTimeType(lastReading)); + updateState(CHANNEL_MEASUREMENT_TIME, new DateTimeType(lastReading)); updateStatus(ThingStatus.ONLINE); break; case GAS: updateState(CHANNEL_GAS, new QuantityType<>(value, SIUnits.CUBIC_METRE)); updateState(CHANNEL_GAS_DAY, new QuantityType<>(dayValue, SIUnits.CUBIC_METRE)); - updateState(CHANNEL_GAS_LAST, new DateTimeType(lastReading)); + updateState(CHANNEL_MEASUREMENT_TIME, new DateTimeType(lastReading)); updateStatus(ThingStatus.ONLINE); break; case WATER: updateState(CHANNEL_WATER, new QuantityType<>(value, SIUnits.CUBIC_METRE)); updateState(CHANNEL_WATER_DAY, new QuantityType<>(dayValue, SIUnits.CUBIC_METRE)); - updateState(CHANNEL_WATER_LAST, new DateTimeType(lastReading)); + updateState(CHANNEL_MEASUREMENT_TIME, new DateTimeType(lastReading)); updateStatus(ThingStatus.ONLINE); break; default: @@ -361,57 +359,63 @@ public void meterReadingEvent(Map meterReadings, Map { - if (v != null) { - double value = (invert ? -1 : 1) * v; - Double dayValue = meterReadingsDay.get(reading); - if (dayValue != null) { - dayValue = (invert ? -1 : 1) * dayValue; - } - switch (reading) { - case NikoHomeControlConstants.NHC_ELECTRICAL_ENERGY: - case NikoHomeControlConstants.NHC_ELECTRICAL_ENERGY_CONSUMPTION: - updateState(CHANNEL_ENERGY, new QuantityType<>(value, KILOWATT_HOUR)); - if (dayValue != null) { - updateState(CHANNEL_ENERGY_DAY, new QuantityType<>(value, KILOWATT_HOUR)); - } - break; - case NikoHomeControlConstants.NHC_ELECTRICAL_ENERGY_FROM_GRID: - updateState(CHANNEL_ENERGY_FROM_GRID, new QuantityType<>(value, KILOWATT_HOUR)); - if (dayValue != null) { - updateState(CHANNEL_ENERGY_FROM_GRID_DAY, new QuantityType<>(value, KILOWATT_HOUR)); - } - break; - case NikoHomeControlConstants.NHC_ELECTRICAL_ENERGY_TO_GRID: - updateState(CHANNEL_ENERGY_TO_GRID, new QuantityType<>(value, KILOWATT_HOUR)); - if (dayValue != null) { - updateState(CHANNEL_ENERGY_TO_GRID_DAY, new QuantityType<>(value, KILOWATT_HOUR)); - } - break; - case NikoHomeControlConstants.NHC_ELECTRICAL_ENERGY_SELF_CONSUMPTION: - updateState(CHANNEL_ENERGY_SELF_CONSUMPTION, new QuantityType<>(value, KILOWATT_HOUR)); - if (dayValue != null) { - updateState(CHANNEL_ENERGY_SELF_CONSUMPTION_DAY, new QuantityType<>(value, KILOWATT_HOUR)); - } - break; - case NikoHomeControlConstants.NHC_GAS_VOLUME: - updateState(CHANNEL_GAS, new QuantityType<>(value, SIUnits.CUBIC_METRE)); - if (dayValue != null) { - updateState(CHANNEL_GAS_DAY, new QuantityType<>(value, SIUnits.CUBIC_METRE)); - } - break; - case NikoHomeControlConstants.NHC_WATER_VOLUME: - updateState(CHANNEL_WATER, new QuantityType<>(value, SIUnits.CUBIC_METRE)); - if (dayValue != null) { - updateState(CHANNEL_WATER_DAY, new QuantityType<>(value, SIUnits.CUBIC_METRE)); - } - break; - default: - break; - } + double value = (invert ? -1 : 1) * v; + Double dayValue = meterReadingsDay.get(reading); + if (dayValue != null) { + dayValue = (invert ? -1 : 1) * dayValue; + } + switch (reading) { + case NikoHomeControlConstants.NHC_ELECTRICAL_ENERGY: + case NikoHomeControlConstants.NHC_ELECTRICAL_ENERGY_CONSUMPTION: + updateState(CHANNEL_ENERGY, new QuantityType<>(value, KILOWATT_HOUR)); + if (dayValue != null) { + updateState(CHANNEL_ENERGY_DAY, new QuantityType<>(value, KILOWATT_HOUR)); + } + break; + case NikoHomeControlConstants.NHC_ELECTRICAL_ENERGY_FROM_GRID: + updateState(CHANNEL_ENERGY_FROM_GRID, new QuantityType<>(value, KILOWATT_HOUR)); + if (dayValue != null) { + updateState(CHANNEL_ENERGY_FROM_GRID_DAY, new QuantityType<>(value, KILOWATT_HOUR)); + } + break; + case NikoHomeControlConstants.NHC_ELECTRICAL_ENERGY_TO_GRID: + updateState(CHANNEL_ENERGY_TO_GRID, new QuantityType<>(value, KILOWATT_HOUR)); + if (dayValue != null) { + updateState(CHANNEL_ENERGY_TO_GRID_DAY, new QuantityType<>(value, KILOWATT_HOUR)); + } + break; + case NikoHomeControlConstants.NHC_ELECTRICAL_ENERGY_SELF_CONSUMPTION: + updateState(CHANNEL_ENERGY_SELF_CONSUMPTION, new QuantityType<>(value, KILOWATT_HOUR)); + if (dayValue != null) { + updateState(CHANNEL_ENERGY_SELF_CONSUMPTION_DAY, new QuantityType<>(value, KILOWATT_HOUR)); + } + break; + case NikoHomeControlConstants.NHC_GAS_VOLUME: + updateState(CHANNEL_GAS, new QuantityType<>(value, SIUnits.CUBIC_METRE)); + if (dayValue != null) { + updateState(CHANNEL_GAS_DAY, new QuantityType<>(value, SIUnits.CUBIC_METRE)); + } + break; + case NikoHomeControlConstants.NHC_WATER_VOLUME: + updateState(CHANNEL_WATER, new QuantityType<>(value, SIUnits.CUBIC_METRE)); + if (dayValue != null) { + updateState(CHANNEL_WATER_DAY, new QuantityType<>(value, SIUnits.CUBIC_METRE)); + } + break; + default: + break; } }); + + if (!meterReadings.isEmpty()) { + updateState(CHANNEL_MEASUREMENT_TIME, new DateTimeType(lastReading)); + updateStatus(ThingStatus.ONLINE); + } } @Override diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcMeter2.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcMeter2.java index 5b956b8d3bfe2..5a01aab8aa2d3 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcMeter2.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcMeter2.java @@ -45,11 +45,6 @@ public class NhcMeter2 extends NhcMeter { protected Map readings = new ConcurrentHashMap<>(); protected Map dayReadings = new ConcurrentHashMap<>(); - protected volatile double fromGridReading; - protected volatile double fromGridDayReading; - protected volatile double toGridReading; - protected volatile double toGridDayReading; - protected NhcMeter2(String id, String name, MeterType meterType, String deviceType, String deviceTechnology, String deviceModel, @Nullable LocalDateTime referenceDate, @Nullable String location, NikoHomeControlCommunication nhcComm, ScheduledExecutorService scheduler) { diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NikoHomeControlCommunication2.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NikoHomeControlCommunication2.java index 67e7424548296..45afbeb50ad6d 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NikoHomeControlCommunication2.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NikoHomeControlCommunication2.java @@ -37,7 +37,6 @@ import java.util.stream.IntStream; import javax.measure.Unit; -import javax.measure.quantity.Energy; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -64,6 +63,7 @@ import org.openhab.core.io.transport.mqtt.MqttException; import org.openhab.core.io.transport.mqtt.MqttMessageSubscriber; import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.SIUnits; import org.openhab.core.library.unit.Units; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -1162,14 +1162,13 @@ private Map parseMeterReadings(String response) { double value = values.getFirst().value; if (unit != null) { try { - Unit receivedUnit = Units.getInstance().getUnit(unit).asType(Energy.class); - Unit targetUnit = Units.getInstance().getUnit("kWh").asType(Energy.class); - if (receivedUnit != null && targetUnit != null) { - QuantityType quantityValue = QuantityType.valueOf(value, receivedUnit) - .toUnit(targetUnit); - if (quantityValue != null) { - value = quantityValue.doubleValue(); - } + Unit receivedUnit = Units.getInstance().getUnit(unit); + Unit targetUnit = receivedUnit.isCompatible(SIUnits.CUBIC_METRE) ? SIUnits.CUBIC_METRE + : Units.KILOWATT_HOUR; + QuantityType quantityValue = QuantityType.valueOf(value, receivedUnit) + .toUnit(targetUnit); + if (quantityValue != null) { + value = quantityValue.doubleValue(); } } catch (ClassCastException e) { logger.debug("Unit conversion failed for unit {}: {}", unit, e.getMessage()); diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml index 5ea5acd8063bf..78edbe461361f 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml @@ -233,7 +233,11 @@ @text/channelEnergySelfConsumptionDayDescription - + + + + + @@ -266,8 +270,11 @@ - + + + 1 + @@ -304,8 +311,11 @@ - + + + 1 + @@ -342,8 +352,11 @@ - + + + 1 + @@ -355,6 +368,12 @@ 10 true + + datetime + + @text/meterConfigStartDateDescription + true + @text/meterConfigInvertDescription @@ -374,8 +393,11 @@ - + + + 1 + @@ -387,6 +409,12 @@ 10 true + + datetime + + @text/meterConfigStartDateDescription + true + @text/meterConfigInvertDescription diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/update/update.xml b/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/update/update.xml new file mode 100644 index 0000000000000..2e70e41ee0300 --- /dev/null +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/update/update.xml @@ -0,0 +1,39 @@ + + + + + + + + nikohomecontrol:measurementtime + + + + + + + + nikohomecontrol:measurementtime + + + + + + + + nikohomecontrol:measurementtime + + + + + + + + nikohomecontrol:measurementtime + + + + + From 028cff6dec7c8bf866d3162953628c6758fb722c Mon Sep 17 00:00:00 2001 From: Mark Herwege Date: Mon, 13 Oct 2025 18:32:40 +0200 Subject: [PATCH 05/13] ssl context Signed-off-by: Mark Herwege --- .../README.md | 6 +- .../internal/SslContextProvider.java | 111 ++++++++++++++++++ .../NikoHomeControlBridgeHandler2.java | 32 ++++- .../NikoHomeControlCarChargerConfig.java | 3 + .../NikoHomeControlCarChargerHandler.java | 73 +++++++++++- .../handler/NikoHomeControlMeterHandler.java | 19 +-- .../HttpClientInitializationException.java | 30 +++++ .../internal/protocol/NhcCarCharger.java | 96 ++++++++++++++- .../internal/protocol/NhcCarChargerEvent.java | 11 ++ .../NikoHomeControlCommunication.java | 15 +++ .../protocol/NikoHomeControlConstants.java | 14 +-- .../protocol/nhc2/NhcCarCharger2.java | 6 +- .../protocol/nhc2/NhcHttpConnection2.java | 23 +++- .../internal/protocol/nhc2/NhcMeter2.java | 2 + .../protocol/nhc2/NhcMqttConnection2.java | 44 +------ .../nhc2/NikoHomeControlCommunication2.java | 58 ++++++--- .../OH-INF/i18n/nikohomecontrol.properties | 2 + .../resources/OH-INF/thing/thing-types.xml | 30 ++++- 18 files changed, 470 insertions(+), 105 deletions(-) create mode 100644 bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/SslContextProvider.java create mode 100644 bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/HttpClientInitializationException.java diff --git a/bundles/org.openhab.binding.nikohomecontrol/README.md b/bundles/org.openhab.binding.nikohomecontrol/README.md index 646524d5cf1bf..3fdfc7c355b95 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/README.md +++ b/bundles/org.openhab.binding.nikohomecontrol/README.md @@ -212,13 +212,13 @@ Thing nikohomecontrol:carCharger:mybridge:mycarcharger [ carChargerId="abcdef01- | powerFromGrid | R | | Number:Power | energyMeterHome | power consumption grid for home energy meter, refreshed every 2s. Linking this channel starts an intensive communication flow with the controller and should only be done when appropriate | | powerToGrid | R | | Number:Power | energyMeterHome | power sent to grid for home energy meter, refreshed every 2s. Linking this channel starts an intensive communication flow with the controller and should only be done when appropriate | | peakPowerFromGrid | R | | Number:Power | energyMeterHome | current month peak power as registered by the home energy meter | -| energy | R | | Number:Energy | energyMeterHome, energyMeterLive, energyMeter | total energy meter reading | -| energyday | R | | Number:Energy | energyMeterHome, energyMeterLive, energyMeter | day energy meter reading | +| energy | R | | Number:Energy | energyMeterHome, energyMeterLive, energyMeter, carCharger | total energy meter reading | +| energyday | R | | Number:Energy | energyMeterHome, energyMeterLive, energyMeter, carCharger | day energy meter reading | | gas | R | | Number:Volume | energyMeterHome, gasMeter | total gas meter reading | | gasday | R | | Number:Volume | energyMeterHome, gasMeter | day gas meter reading | | water | R | | Number:Volume | energyMeterHome, waterMeter | total water meter reading | | waterday | R | | Number:Volume | energyMeterHome, waterMeter | day water meter reading | -| measurementtime | R | | DateTimeType | energyMeterHome, energyMeterLive, energyMeter, gasMeter, waterMeter | last meter reading time | +| measurementtime | R | | DateTimeType | energyMeterHome, energyMeterLive, energyMeter, gasMeter, waterMeter, carCharger | last meter reading time | | bellbutton | RW | | Switch | access, accessRingAndComeIn | bell button connected to access device, including buttons on video phone devices linked to an access device. The bell can also be triggered by an `ON` command, `autoupdate="false"` by default | | ringandcomein | RW | | Switch | accessRingAndComeIn | provide state and turn automatic door unlocking at bell ring on/off | | lock | RW | | Switch | access, accessRingAndComeIn | provide doorlock state and unlock the door by sending an `OFF` command. `autoupdate="false"` by default | diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/SslContextProvider.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/SslContextProvider.java new file mode 100644 index 0000000000000..56aba05c1d1e1 --- /dev/null +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/SslContextProvider.java @@ -0,0 +1,111 @@ +package org.openhab.binding.nikohomecontrol.internal; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ResourceBundle; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Utility to provide a shared SSLContext and Jetty SslContextFactory + * that trusts the built-in Niko Home Control controller certificates. + * + * @author Mark Herwege - Initial Contribution + */ +@NonNullByDefault +public final class SslContextProvider { + + private static final Logger LOGGER = LoggerFactory.getLogger(SslContextProvider.class); + + private @Nullable static SSLContext sslContext; + private @Nullable static TrustManager @Nullable [] trustManagers = null; + + private SslContextProvider() { + } + + public static synchronized SSLContext getSSLContext() throws CertificateException { + SSLContext context = sslContext; + if (context == null) { + context = buildSSLContext(); + } + sslContext = context; + return context; + } + + public static synchronized TrustManager[] getTrustManagers() throws CertificateException { + TrustManager[] managers = trustManagers; + if (managers == null) { + managers = importCertificates(); + } + trustManagers = managers; + return managers; + } + + public static synchronized SslContextFactory.Client getSslContextFactory() throws CertificateException { + SslContextFactory.Client factory = new SslContextFactory.Client(); + factory.setSslContext(getSSLContext()); + factory.setEndpointIdentificationAlgorithm(null); // disable hostname verification (allows IP to be used) + return factory; + } + + private static SSLContext buildSSLContext() throws CertificateException { + SSLContext context; + try { + context = SSLContext.getInstance("TLS"); + context.init(null, getTrustManagers(), null); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + LOGGER.debug("error with SSL context creation: {}", e.getMessage()); + throw new CertificateException("SSL context creation exception", e); + } + + LOGGER.debug("Initialized SSLContext with embedded Niko Home Control certificates"); + return context; + } + + private static TrustManager[] importCertificates() throws CertificateException { + ResourceBundle certificatesBundle = ResourceBundle.getBundle("nikohomecontrol/certificates"); + + try { + // Load server public certificates into key store + CertificateFactory cf = CertificateFactory.getInstance("X509"); + InputStream certificateStream; + final KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null, null); + for (String certName : certificatesBundle.keySet()) { + certificateStream = new ByteArrayInputStream( + certificatesBundle.getString(certName).getBytes(StandardCharsets.UTF_8)); + X509Certificate certificate = (X509Certificate) cf.generateCertificate(certificateStream); + keyStore.setCertificateEntry(certName, certificate); + } + + ResourceBundle.clearCache(); + + // Create trust managers used to validate server + TrustManagerFactory tmFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmFactory.init(keyStore); + return tmFactory.getTrustManagers(); + } catch (CertificateException | KeyStoreException | NoSuchAlgorithmException | IOException e) { + LOGGER.debug("error with SSL context creation: {} ", e.getMessage()); + throw new CertificateException("SSL context creation exception", e); + } finally { + ResourceBundle.clearCache(); + } + } +} diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBridgeHandler2.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBridgeHandler2.java index d192877585e58..2aa0a729ca7f8 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBridgeHandler2.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBridgeHandler2.java @@ -24,6 +24,9 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.openhab.binding.nikohomecontrol.internal.NikoHomeControlBindingConstants; +import org.openhab.binding.nikohomecontrol.internal.SslContextProvider; import org.openhab.binding.nikohomecontrol.internal.protocol.nhc2.NikoHomeControlCommunication2; import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.io.net.http.HttpClientFactory; @@ -51,12 +54,13 @@ public class NikoHomeControlBridgeHandler2 extends NikoHomeControlBridgeHandler private final Gson gson = new GsonBuilder().create(); - private final HttpClient httpClient; + private HttpClientFactory httpClientFactory; + private @Nullable HttpClient httpClient; public NikoHomeControlBridgeHandler2(Bridge nikoHomeControlBridge, NetworkAddressService networkAddressService, TimeZoneProvider timeZoneProvider, HttpClientFactory httpClientFactory) { super(nikoHomeControlBridge, networkAddressService, timeZoneProvider); - httpClient = httpClientFactory.getCommonHttpClient(); + this.httpClientFactory = httpClientFactory; } @Override @@ -90,6 +94,12 @@ public void initialize() { addr = (addr == null) ? "unknown" : addr.replace(".", "_"); String clientId = addr + "-" + thing.getUID().toString().replace(":", "_"); try { + HttpClient httpClient = this.httpClient; + if (httpClient == null) { + SslContextFactory.Client sslContextFactory = SslContextProvider.getSslContextFactory(); + httpClient = httpClientFactory.createHttpClient(NikoHomeControlBindingConstants.BINDING_ID, + sslContextFactory); + } nhcComm = new NikoHomeControlCommunication2(this, clientId, scheduler, httpClient); startCommunication(); } catch (CertificateException e) { @@ -100,6 +110,20 @@ public void initialize() { } } + @Override + public void dispose() { + HttpClient httpClient = this.httpClient; + if (httpClient != null) { + try { + httpClient.stop(); + } catch (Exception e) { + // Nothing to do + } + } + this.httpClient = null; + super.dispose(); + } + @Override protected void updateProperties() { Map properties = new HashMap<>(thing.getProperties()); @@ -177,10 +201,6 @@ public String getToken() { return token; } - public HttpClient getHttpClient() { - return httpClient; - } - /** * Extract the expiry date in the user provided token for the hobby API. Log warnings and errors if the token is * close to expiry or expired. diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlCarChargerConfig.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlCarChargerConfig.java index 2a5fa1791cf77..7c8519cbf6d4f 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlCarChargerConfig.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlCarChargerConfig.java @@ -22,4 +22,7 @@ @NonNullByDefault public class NikoHomeControlCarChargerConfig { public String carChargerId = ""; + public int refresh = 10; + public String startDate = ""; + public boolean invert = false; } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlCarChargerHandler.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlCarChargerHandler.java index 966243eb82cd9..68377523b9539 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlCarChargerHandler.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlCarChargerHandler.java @@ -14,9 +14,17 @@ import static org.openhab.binding.nikohomecontrol.internal.NikoHomeControlBindingConstants.*; import static org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlConstants.*; +import static org.openhab.core.library.unit.Units.KILOWATT_HOUR; import static org.openhab.core.types.RefreshType.REFRESH; import java.math.BigDecimal; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoUnit; import java.util.HashMap; import java.util.Map; @@ -26,6 +34,7 @@ import org.openhab.binding.nikohomecontrol.internal.protocol.NhcCarChargerEvent; import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlCommunication; import org.openhab.binding.nikohomecontrol.internal.protocol.nhc2.NhcCarCharger2; +import org.openhab.core.config.core.Configuration; import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.OnOffType; @@ -91,8 +100,16 @@ void handleCommandSelection(ChannelUID channelUID, Command command) { nhcCarCharger.getTargetTime(), nhcCarCharger.isBoost(), nhcCarCharger.getReachableDistance(), nhcCarCharger.getNextChargingTime()); break; + case CHANNEL_ENERGY: + case CHANNEL_ENERGY_DAY: + case CHANNEL_MEASUREMENT_TIME: + LocalDateTime lastReadingUTC = nhcCarCharger.getLastReading(); + if (lastReadingUTC != null) { + meterReadingEvent(nhcCarCharger.getReading(), nhcCarCharger.getDayReading(), lastReadingUTC); + } + break; default: - + break; } } else { switch (channelUID.getId()) { @@ -188,6 +205,22 @@ synchronized void startCommunication() { return; } + String startDate = getConfig().as(NikoHomeControlMeterConfig.class).startDate; + if (startDate.isEmpty()) { + startDate = Instant.now().atZone(nhcComm.getTimeZone()).truncatedTo(ChronoUnit.DAYS) + .format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); + Configuration config = editConfiguration(); + config.put(CONFIG_METER_START_DATE, startDate); + updateConfiguration(config); + } + try { + LocalDateTime.parse(startDate); + } catch (DateTimeParseException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.configuration-error.meterStartDate"); + return; + } + nhcCarCharger.setEventHandler(this); updateProperties(nhcCarCharger); @@ -212,15 +245,21 @@ void refresh() { chargingStatusEvent(carCharger.getStatus(), carCharger.getChargingStatus(), carCharger.getEvStatus(), carCharger.getCouplingStatus(), carCharger.getElectricalPower()); } + NikoHomeControlCommunication nhcComm = getCommunication(getBridgeHandler()); + if (nhcComm != null) { + NikoHomeControlMeterConfig config = getConfig().as(NikoHomeControlMeterConfig.class); + nhcComm.startMeter(deviceId, config.refresh, config.startDate); + } } @Override public void dispose() { NikoHomeControlCommunication nhcComm = getCommunication(getBridgeHandler()); if (nhcComm != null) { - NhcCarCharger access = nhcComm.getCarChargerDevices().get(deviceId); - if (access != null) { - access.unsetEventHandler(); + nhcComm.stopMeter(deviceId); + NhcCarCharger carCharger = nhcComm.getCarChargerDevices().get(deviceId); + if (carCharger != null) { + carCharger.unsetEventHandler(); } } nhcCarCharger = null; @@ -288,4 +327,30 @@ public void chargingModeEvent(@Nullable String chargingMode, float targetDistanc } updateStatus(ThingStatus.ONLINE); } + + @Override + public void meterReadingEvent(double reading, double dayReading, LocalDateTime lastReadingUTC) { + NhcCarCharger nhcCarCharger = this.nhcCarCharger; + if (nhcCarCharger == null) { + logger.debug("car charger device with ID {} not initialized", deviceId); + return; + } + + NikoHomeControlBridgeHandler bridgeHandler = getBridgeHandler(); + if (bridgeHandler == null) { + logger.debug("Cannot update char charger channels, no bridge handler"); + return; + } + + ZonedDateTime lastReading = lastReadingUTC.atZone(ZoneOffset.UTC) + .withZoneSameInstant(bridgeHandler.getTimeZone()); + + boolean invert = getConfig().as(NikoHomeControlCarChargerConfig.class).invert; + double value = (invert ? -1 : 1) * reading; + double dayValue = (invert ? -1 : 1) * dayReading; + updateState(CHANNEL_ENERGY, new QuantityType<>(value, KILOWATT_HOUR)); + updateState(CHANNEL_ENERGY_DAY, new QuantityType<>(dayValue, KILOWATT_HOUR)); + updateState(CHANNEL_MEASUREMENT_TIME, new DateTimeType(lastReading)); + updateStatus(ThingStatus.ONLINE); + } } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlMeterHandler.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlMeterHandler.java index 9caa10cf70a4c..c8c8e9216e63b 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlMeterHandler.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlMeterHandler.java @@ -150,8 +150,9 @@ synchronized void startCommunication() { } MeterType meterType = nhcMeter.getType(); - if (!(MeterType.ENERGY_LIVE.equals(meterType) || MeterType.ENERGY.equals(meterType) - || MeterType.GAS.equals(meterType) || MeterType.WATER.equals(meterType))) { + if (!(MeterType.ENERGY_HOME.equals(meterType) || MeterType.ENERGY_LIVE.equals(meterType) + || MeterType.ENERGY.equals(meterType) || MeterType.GAS.equals(meterType) + || MeterType.WATER.equals(meterType))) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/offline.configuration-error.meterType"); return; @@ -261,7 +262,7 @@ public void meterPowerEvent(@Nullable Double power, @Nullable Double powerFromGr } MeterType meterType = nhcMeter.getType(); - if (meterType != MeterType.ENERGY_LIVE) { + if (meterType != MeterType.ENERGY_LIVE && meterType != MeterType.ENERGY_HOME) { logger.debug("meter with ID {} does not support live readings", deviceId); return; } @@ -374,37 +375,37 @@ public void meterReadingEvent(Map meterReadings, Map(value, KILOWATT_HOUR)); if (dayValue != null) { - updateState(CHANNEL_ENERGY_DAY, new QuantityType<>(value, KILOWATT_HOUR)); + updateState(CHANNEL_ENERGY_DAY, new QuantityType<>(dayValue, KILOWATT_HOUR)); } break; case NikoHomeControlConstants.NHC_ELECTRICAL_ENERGY_FROM_GRID: updateState(CHANNEL_ENERGY_FROM_GRID, new QuantityType<>(value, KILOWATT_HOUR)); if (dayValue != null) { - updateState(CHANNEL_ENERGY_FROM_GRID_DAY, new QuantityType<>(value, KILOWATT_HOUR)); + updateState(CHANNEL_ENERGY_FROM_GRID_DAY, new QuantityType<>(dayValue, KILOWATT_HOUR)); } break; case NikoHomeControlConstants.NHC_ELECTRICAL_ENERGY_TO_GRID: updateState(CHANNEL_ENERGY_TO_GRID, new QuantityType<>(value, KILOWATT_HOUR)); if (dayValue != null) { - updateState(CHANNEL_ENERGY_TO_GRID_DAY, new QuantityType<>(value, KILOWATT_HOUR)); + updateState(CHANNEL_ENERGY_TO_GRID_DAY, new QuantityType<>(dayValue, KILOWATT_HOUR)); } break; case NikoHomeControlConstants.NHC_ELECTRICAL_ENERGY_SELF_CONSUMPTION: updateState(CHANNEL_ENERGY_SELF_CONSUMPTION, new QuantityType<>(value, KILOWATT_HOUR)); if (dayValue != null) { - updateState(CHANNEL_ENERGY_SELF_CONSUMPTION_DAY, new QuantityType<>(value, KILOWATT_HOUR)); + updateState(CHANNEL_ENERGY_SELF_CONSUMPTION_DAY, new QuantityType<>(dayValue, KILOWATT_HOUR)); } break; case NikoHomeControlConstants.NHC_GAS_VOLUME: updateState(CHANNEL_GAS, new QuantityType<>(value, SIUnits.CUBIC_METRE)); if (dayValue != null) { - updateState(CHANNEL_GAS_DAY, new QuantityType<>(value, SIUnits.CUBIC_METRE)); + updateState(CHANNEL_GAS_DAY, new QuantityType<>(dayValue, SIUnits.CUBIC_METRE)); } break; case NikoHomeControlConstants.NHC_WATER_VOLUME: updateState(CHANNEL_WATER, new QuantityType<>(value, SIUnits.CUBIC_METRE)); if (dayValue != null) { - updateState(CHANNEL_WATER_DAY, new QuantityType<>(value, SIUnits.CUBIC_METRE)); + updateState(CHANNEL_WATER_DAY, new QuantityType<>(dayValue, SIUnits.CUBIC_METRE)); } break; default: diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/HttpClientInitializationException.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/HttpClientInitializationException.java new file mode 100644 index 0000000000000..de6a27cf7ee39 --- /dev/null +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/HttpClientInitializationException.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2010-2025 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.nikohomecontrol.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * {@link HttpClientInitializationException} used for HTTP connection exceptions. + * + * @author Mark Herwege - Initial Contribution + */ +@NonNullByDefault +public class HttpClientInitializationException extends Exception { + + private static final long serialVersionUID = 6823948572039847561L; + + public HttpClientInitializationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcCarCharger.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcCarCharger.java index c2f8c6599fbfd..32f81f798211d 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcCarCharger.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcCarCharger.java @@ -12,6 +12,12 @@ */ package org.openhab.binding.nikohomecontrol.internal.protocol; +import java.time.LocalDateTime; +import java.util.Random; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.slf4j.Logger; @@ -47,15 +53,24 @@ public abstract class NhcCarCharger { protected volatile boolean boost; protected volatile float reachableDistance; protected volatile @Nullable String nextChargingTime; + protected volatile double reading; + protected volatile double dayReading; + protected volatile @Nullable LocalDateTime lastReadingUTC; + + private @Nullable NhcCarChargerEvent eventHandler; + + private ScheduledExecutorService scheduler; + private volatile @Nullable ScheduledFuture readingSchedule; - @Nullable - private NhcCarChargerEvent eventHandler; + private Random r = new Random(); - protected NhcCarCharger(String id, String name, @Nullable String location, NikoHomeControlCommunication nhcComm) { + protected NhcCarCharger(String id, String name, @Nullable String location, NikoHomeControlCommunication nhcComm, + ScheduledExecutorService scheduler) { this.id = id; this.name = name; this.location = location; this.nhcComm = nhcComm; + this.scheduler = scheduler; } /** @@ -266,6 +281,34 @@ public void setStatus(boolean status, @Nullable String chargingStatus, @Nullable } } + /** + * @return the meter reading in kWh + */ + public double getReading() { + return reading; + } + + /** + * @return the meter reading for the current day in kWh + */ + public double getDayReading() { + return dayReading; + } + + /** + * @return last processed meter reading in UTC zone + */ + public @Nullable LocalDateTime getLastReading() { + return lastReadingUTC; + } + + public void setReading(double reading, double dayReading, LocalDateTime lastReading) { + this.reading = reading; + this.dayReading = dayReading; + this.lastReadingUTC = lastReading; + updateReadingState(); + } + /** * Updates the charging mode and related channels for the car charger thing. * If a parameter is {@code null}, the existing value is retained. @@ -299,10 +342,26 @@ public void setChargingMode(@Nullable String chargingMode, @Nullable Float targe } } + /** + * Update meter reading value of the meter without touching the meter definition (id, name) and without changing the + * ThingHandler callback. + * + */ + protected void updateReadingState() { + NhcCarChargerEvent handler = eventHandler; + double reading = getReading(); + double dayReading = getDayReading(); + LocalDateTime lastReading = getLastReading(); + if ((handler != null) && (lastReading != null)) { + logger.debug("update meter reading channels for {} with {}, day {}", id, reading, dayReading); + handler.meterReadingEvent(reading, dayReading, lastReading); + } + } + /** * Method called when car charger is removed from the Niko Home Control Controller. */ - public void carChargerDeviceRemoved() { + public void carChargerRemoved() { logger.debug("car charger removed {}, {}", id, name); NhcCarChargerEvent eventHandler = this.eventHandler; if (eventHandler != null) { @@ -341,11 +400,38 @@ public void executeCarChargerChargingMode(String chargingMode, float targetDista * Executes a command to change the charging boost state for SMART charging of the car charger in the Niko Home * Control system. * When boost is true, the capacity limit may not be respected. - * + * * @param boost true to enable charging boost, false to disable it */ public void executeCarChargerChargingBoost(boolean boost) { logger.debug("change car charger boost for {} to {}", id, boost); nhcComm.executeCarChargerChargingBoost(id, boost); } + + /** + * Start receiving meter data at regular intervals. Initial delay will be random between 10 and 100s to reduce risk + * of overloading controller during initialization. + * + * @param refresh interval between meter queries in minutes + */ + public void startMeter(int refresh, String startDate) { + stopMeter(); + int firstRefreshDelay = 10 + r.nextInt(90); + logger.debug("schedule meter data refresh for {} every {} minutes, first refresh in {}s", id, refresh, + firstRefreshDelay); + readingSchedule = scheduler.scheduleWithFixedDelay(() -> { + nhcComm.executeMeter(id, startDate); + }, firstRefreshDelay, refresh * 60, TimeUnit.SECONDS); + } + + /** + * Stop receiving meter data. + */ + public void stopMeter() { + ScheduledFuture schedule = readingSchedule; + if (schedule != null) { + schedule.cancel(true); + readingSchedule = null; + } + } } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcCarChargerEvent.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcCarChargerEvent.java index a7908e7bad59d..a070af6536d38 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcCarChargerEvent.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcCarChargerEvent.java @@ -12,6 +12,8 @@ */ package org.openhab.binding.nikohomecontrol.internal.protocol; +import java.time.LocalDateTime; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -52,4 +54,13 @@ void chargingStatusEvent(boolean status, @Nullable String chargingStatus, @Nulla */ void chargingModeEvent(@Nullable String chargingMode, float targetDistance, @Nullable String targetTime, boolean boost, float reachableDistance, @Nullable String nextChargingTime); + + /** + * This method is called when a meter reading is received from the Niko Home Control controller. + * + * @param reading meter reading + * @param dayReading meter reading for current day + * @param lastReadingUTC last meter reading date and time, UTC + */ + void meterReadingEvent(double reading, double dayReading, LocalDateTime lastReadingUTC); } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NikoHomeControlCommunication.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NikoHomeControlCommunication.java index 444015c59fb99..ab8e150d4d01b 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NikoHomeControlCommunication.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NikoHomeControlCommunication.java @@ -292,6 +292,12 @@ public void startMeter(String meterId, int refresh, String startDate) { NhcMeter meter = getMeters().get(meterId); if (meter != null) { meter.startMeter(refresh, startDate); + return; + } + NhcCarCharger carCharger = getCarChargerDevices().get(meterId); + if (carCharger != null) { + carCharger.startMeter(refresh, startDate); + return; } } @@ -302,6 +308,12 @@ public void stopMeter(String meterId) { NhcMeter meter = getMeters().get(meterId); if (meter != null) { meter.stopMeter(); + return; + } + NhcCarCharger carCharger = getCarChargerDevices().get(meterId); + if (carCharger != null) { + carCharger.stopMeter(); + return; } } @@ -313,6 +325,9 @@ public void stopAllMeters() { stopMeter(meterId); stopMeterLive(meterId); } + for (String carChargerId : getCarChargerDevices().keySet()) { + stopMeter(carChargerId); + } } /** diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NikoHomeControlConstants.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NikoHomeControlConstants.java index b4e06e4961247..4a420f2132415 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NikoHomeControlConstants.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NikoHomeControlConstants.java @@ -128,11 +128,11 @@ public static enum MeterType { "UNKNOWN ERROR"); // NhcII energy channels - public static final String NHC_ELECTRICAL_ENERGY = "electricalEnergy"; - public static final String NHC_ELECTRICAL_ENERGY_CONSUMPTION = "electricalEnergyConsumption"; - public static final String NHC_ELECTRICAL_ENERGY_TO_GRID = "electricalEnergyToGrid"; - public static final String NHC_ELECTRICAL_ENERGY_FROM_GRID = "electricalEnergyFromGrid"; - public static final String NHC_ELECTRICAL_ENERGY_SELF_CONSUMPTION = "electricalEnergySelfConsumption"; - public static final String NHC_GAS_VOLUME = "gasVolume"; - public static final String NHC_WATER_VOLUME = "waterVolume"; + public static final String NHC_ELECTRICAL_ENERGY = "ElectricalEnergy"; + public static final String NHC_ELECTRICAL_ENERGY_CONSUMPTION = "ElectricalEnergyConsumption"; + public static final String NHC_ELECTRICAL_ENERGY_TO_GRID = "ElectricalEnergyToGrid"; + public static final String NHC_ELECTRICAL_ENERGY_FROM_GRID = "ElectricalEnergyFromGrid"; + public static final String NHC_ELECTRICAL_ENERGY_SELF_CONSUMPTION = "ElectricalEnergySelfConsumption"; + public static final String NHC_GAS_VOLUME = "GasVolume"; + public static final String NHC_WATER_VOLUME = "WaterVolume"; } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcCarCharger2.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcCarCharger2.java index efc872045f7e2..afcf2a6e0d33a 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcCarCharger2.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcCarCharger2.java @@ -12,6 +12,8 @@ */ package org.openhab.binding.nikohomecontrol.internal.protocol.nhc2; +import java.util.concurrent.ScheduledExecutorService; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.nikohomecontrol.internal.protocol.NhcCarCharger; @@ -32,8 +34,8 @@ public class NhcCarCharger2 extends NhcCarCharger { private final String deviceModel; NhcCarCharger2(String id, String name, String deviceType, String deviceTechnology, String deviceModel, - @Nullable String location, NikoHomeControlCommunication nhcComm) { - super(id, name, location, nhcComm); + @Nullable String location, NikoHomeControlCommunication nhcComm, ScheduledExecutorService scheduler) { + super(id, name, location, nhcComm, scheduler); this.deviceType = deviceType; this.deviceTechnology = deviceTechnology; this.deviceModel = deviceModel; diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcHttpConnection2.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcHttpConnection2.java index 8488464533492..63983a9b91275 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcHttpConnection2.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcHttpConnection2.java @@ -25,6 +25,7 @@ import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; +import org.openhab.binding.nikohomecontrol.internal.protocol.HttpClientInitializationException; import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,19 +53,29 @@ public class NhcHttpConnection2 { private static final long TIMEOUT_MS = 1000; - NhcHttpConnection2(HttpClient httpClient, String cocoAddress, String token) { + NhcHttpConnection2(HttpClient httpClient, String cocoAddress, String token) + throws HttpClientInitializationException { this.httpClient = httpClient; this.token = token; this.hostname = cocoAddress; - measurementsBaseUrl = "https://" + cocoAddress + NikoHomeControlConstants.NHC_MEASUREMENTS_BASEURL; + measurementsBaseUrl = String.format("https://%s%s", cocoAddress, + NikoHomeControlConstants.NHC_MEASUREMENTS_BASEURL); + if (!httpClient.isStarted()) { + try { + httpClient.start(); + } catch (Exception e) { + logger.debug("Failed to start http connection to Niko Home Control controller at {}", cocoAddress, e); + throw new HttpClientInitializationException("Failed to start HTTP client", e); + } + } } public @Nullable String getMeasurements(String deviceUuid, LocalDateTime start, LocalDateTime end) { String intervalStart = start.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); String intervalEnd = end.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); - String url = measurementsBaseUrl + "/" + deviceUuid + "/total?Aggregation=sum&IntervalStart=" + intervalStart - + "&IntervalEnd=" + intervalEnd; - logger.trace("Get Total Measurements: {}", url); + String url = String.format("%s/%s/total?Aggregation=sum&IntervalStart=%s&IntervalEnd=%s", measurementsBaseUrl, + deviceUuid, intervalStart, intervalEnd); + logger.trace("Get measurements: {}", url); Request request = httpClient.newRequest(url); ContentResponse response; try { @@ -72,7 +83,7 @@ public class NhcHttpConnection2 { .header(HttpHeader.HOST, hostname).header(HttpHeader.ACCEPT, "application/json") .timeout(TIMEOUT_MS, TimeUnit.MILLISECONDS).send(); } catch (InterruptedException | TimeoutException | ExecutionException e) { - logger.debug("Error with query {}", url); + logger.debug("Error with query {}", url, e); return null; } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcMeter2.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcMeter2.java index 5a01aab8aa2d3..f381b99b75272 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcMeter2.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcMeter2.java @@ -90,6 +90,8 @@ public void setReadings(Map readings, Map dayRea if (readings.isEmpty() || dayReadings.isEmpty()) { return; } + this.readings = readings; + this.dayReadings = dayReadings; this.lastReadingUTC = lastReading; updateReadingState(); } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcMqttConnection2.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcMqttConnection2.java index 7eb4291c3d9d7..8c41522f954fe 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcMqttConnection2.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcMqttConnection2.java @@ -12,27 +12,17 @@ */ package org.openhab.binding.nikohomecontrol.internal.protocol.nhc2; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.util.ResourceBundle; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.nikohomecontrol.internal.SslContextProvider; import org.openhab.core.io.transport.mqtt.MqttActionCallback; import org.openhab.core.io.transport.mqtt.MqttBrokerConnection; import org.openhab.core.io.transport.mqtt.MqttConnectionObserver; @@ -72,42 +62,12 @@ public class NhcMqttConnection2 implements MqttActionCallback { NhcMqttConnection2(String clientId, MqttMessageSubscriber messageSubscriber, MqttConnectionObserver connectionObserver) throws CertificateException { - trustManagers = getTrustManagers(); + trustManagers = SslContextProvider.getTrustManagers(); this.clientId = clientId; this.messageSubscriber = messageSubscriber; this.connectionObserver = connectionObserver; } - private TrustManager[] getTrustManagers() throws CertificateException { - ResourceBundle certificatesBundle = ResourceBundle.getBundle("nikohomecontrol/certificates"); - - try { - // Load server public certificates into key store - CertificateFactory cf = CertificateFactory.getInstance("X509"); - InputStream certificateStream; - final KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); - keyStore.load(null, null); - for (String certName : certificatesBundle.keySet()) { - certificateStream = new ByteArrayInputStream( - certificatesBundle.getString(certName).getBytes(StandardCharsets.UTF_8)); - X509Certificate certificate = (X509Certificate) cf.generateCertificate(certificateStream); - keyStore.setCertificateEntry(certName, certificate); - } - - ResourceBundle.clearCache(); - - // Create trust managers used to validate server - TrustManagerFactory tmFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - tmFactory.init(keyStore); - return tmFactory.getTrustManagers(); - } catch (CertificateException | KeyStoreException | NoSuchAlgorithmException | IOException e) { - logger.debug("error with SSL context creation: {} ", e.getMessage()); - throw new CertificateException("SSL context creation exception", e); - } finally { - ResourceBundle.clearCache(); - } - } - /** * Start a secure MQTT connection and subscribe to all topics. * diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NikoHomeControlCommunication2.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NikoHomeControlCommunication2.java index 45afbeb50ad6d..77deb226d7ae9 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NikoHomeControlCommunication2.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NikoHomeControlCommunication2.java @@ -18,6 +18,7 @@ import java.net.InetAddress; import java.security.cert.CertificateException; import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.ArrayList; @@ -41,6 +42,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.nikohomecontrol.internal.protocol.HttpClientInitializationException; import org.openhab.binding.nikohomecontrol.internal.protocol.NhcAccess; import org.openhab.binding.nikohomecontrol.internal.protocol.NhcAction; import org.openhab.binding.nikohomecontrol.internal.protocol.NhcAlarm; @@ -151,14 +153,22 @@ public synchronized void startCommunication() { return; } - httpConnection = new NhcHttpConnection2(httpClient, addrString, token); + try { + httpConnection = new NhcHttpConnection2(httpClient, addrString, token); + } catch (HttpClientInitializationException e) { + logger.debug("error initializing http communication"); + handler.controllerOffline("@text/offline.communication-error"); + scheduleRestartCommunication(); + return; + } try { mqttConnection.startConnection(addrString, port, profile, token); } catch (MqttException e) { - logger.debug("error in mqtt communication"); + logger.debug("error initializing mqtt communication"); handler.controllerOffline("@text/offline.communication-error"); scheduleRestartCommunication(); + return; } } @@ -634,7 +644,7 @@ private void addCarChargerDevice(NhcDevice2 device, @Nullable String location) { } else { logger.debug("adding car charger device {} model {}, {}", device.uuid, device.model, device.name); nhcCarCharger = new NhcCarCharger2(device.uuid, device.name, device.type, device.technology, device.model, - location, this); + location, this, scheduler); } carChargerDevices.put(device.uuid, nhcCarCharger); } @@ -666,7 +676,7 @@ private void removeDevice(NhcDevice2 device) { alarm.alarmDeviceRemoved(); alarmDevices.remove(device.uuid); } else if (carCharger != null) { - carCharger.carChargerDeviceRemoved(); + carCharger.carChargerRemoved(); carChargerDevices.remove(device.uuid); } } @@ -1117,7 +1127,8 @@ public void executeThermostat(String thermostatId, int overruleTemp, int overrul @Override public void executeMeter(String meterId, String startDate) { NhcMeter meter = meters.get(meterId); - if (meter == null) { + NhcCarCharger carCharger = carChargerDevices.get(meterId); + if (meter == null && carCharger == null) { return; } @@ -1125,12 +1136,10 @@ public void executeMeter(String meterId, String startDate) { if (httpConnection == null) { return; } - LocalDateTime now = ZonedDateTime.now().withZoneSameInstant(getTimeZone()).toLocalDateTime(); + ZonedDateTime nowUTC = ZonedDateTime.now().withZoneSameInstant(ZoneOffset.UTC); + LocalDateTime now = nowUTC.withZoneSameInstant(getTimeZone()).toLocalDateTime(); + LocalDateTime meterStart = LocalDateTime.parse(startDate); LocalDateTime dayStart = now.truncatedTo(ChronoUnit.DAYS); - LocalDateTime meterStart = meter.getLastReading(); - if (meterStart == null) { - meterStart = LocalDateTime.parse(startDate); - } String response = httpConnection.getMeasurements(meterId, meterStart, now); logger.trace("Meter measurement: {}", response); @@ -1140,7 +1149,16 @@ public void executeMeter(String meterId, String startDate) { logger.trace("Day measurement: {}", response); final Map dayReadings = response != null ? parseMeterReadings(response) : Map.of(); - meter.setReadings(readings, dayReadings, now); + if (meter != null) { + meter.setReadings(readings, dayReadings, nowUTC.toLocalDateTime()); + } + if (carCharger != null) { + Double reading = readings.values().stream().findFirst().orElse(null); + Double dayReading = dayReadings.values().stream().findFirst().orElse(null); + if (reading != null && dayReading != null) { + carCharger.setReading(reading, dayReading, nowUTC.toLocalDateTime()); + } + } } private Map parseMeterReadings(String response) { @@ -1160,15 +1178,19 @@ private Map parseMeterReadings(String response) { List values = r.values; if (property != null && values != null && !values.isEmpty()) { double value = values.getFirst().value; - if (unit != null) { + if (unit != null && !unit.isEmpty()) { try { Unit receivedUnit = Units.getInstance().getUnit(unit); - Unit targetUnit = receivedUnit.isCompatible(SIUnits.CUBIC_METRE) ? SIUnits.CUBIC_METRE - : Units.KILOWATT_HOUR; - QuantityType quantityValue = QuantityType.valueOf(value, receivedUnit) - .toUnit(targetUnit); - if (quantityValue != null) { - value = quantityValue.doubleValue(); + if (receivedUnit != null) { + Unit targetUnit = receivedUnit.isCompatible(Units.KILOWATT_HOUR) + ? Units.KILOWATT_HOUR + : (receivedUnit.isCompatible(SIUnits.CUBIC_METRE) ? SIUnits.CUBIC_METRE + : receivedUnit); + QuantityType quantityValue = QuantityType.valueOf(value, receivedUnit) + .toUnit(targetUnit); + if (quantityValue != null) { + value = quantityValue.doubleValue(); + } } } catch (ClassCastException e) { logger.debug("Unit conversion failed for unit {}: {}", unit, e.getMessage()); diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/i18n/nikohomecontrol.properties b/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/i18n/nikohomecontrol.properties index e2562fff73113..da51f1c5fd305 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/i18n/nikohomecontrol.properties +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/i18n/nikohomecontrol.properties @@ -191,6 +191,8 @@ channelOptionAlarmStateArmed = Armed channelOptionAlarmStatePreAlarm = Pre-alarm channelOptionAlarmStateAlarm = Alarm +channelCarChargerStatusLabel = Car Charger Status +channelCarChargerStatusDescription = Car charger on/off status channelChargingStatusLabel = Car Charging Status channelChargingStatusDescription = Car charger charging status channelOptionChargingStatusActive = Active diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml index 78edbe461361f..37734f54585ee 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml @@ -218,8 +218,8 @@ @text/channelEnergyFromGridDescription - - @text/channelEnergyFromGridDayDescription + + @text/channelEnergyToGridDayDescription @@ -488,7 +488,10 @@ @text/carChargerDescription EVSE - + + + @text/channelCarChargerStatusDescription + @@ -499,12 +502,33 @@ + + + @text/deviceConfigDeviceIdDescription + + + @text/meterConfigRefreshDescription + 10 + true + + + datetime + + @text/meterConfigStartDateDescription + true + + + + @text/meterConfigInvertDescription + false + true + From 2e18a19ee4a725aa05bceaca30b432757354ca59 Mon Sep 17 00:00:00 2001 From: Mark Herwege Date: Mon, 13 Oct 2025 18:38:54 +0200 Subject: [PATCH 06/13] comment Signed-off-by: Mark Herwege --- .../internal/handler/NikoHomeControlBridgeHandler2.java | 1 + 1 file changed, 1 insertion(+) diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBridgeHandler2.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBridgeHandler2.java index 2aa0a729ca7f8..6b3f3469f2ab0 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBridgeHandler2.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlBridgeHandler2.java @@ -46,6 +46,7 @@ * to the framework. * * @author Mark Herwege - Initial Contribution + * @author Mark Herwege - Add http connection to controller */ @NonNullByDefault public class NikoHomeControlBridgeHandler2 extends NikoHomeControlBridgeHandler { From d58931dfb13acdca0a275c095647b859e62a3b56 Mon Sep 17 00:00:00 2001 From: Mark Herwege Date: Mon, 13 Oct 2025 20:23:38 +0200 Subject: [PATCH 07/13] add header Signed-off-by: Mark Herwege --- .../nikohomecontrol/internal/SslContextProvider.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/SslContextProvider.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/SslContextProvider.java index 56aba05c1d1e1..98fb84042e84b 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/SslContextProvider.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/SslContextProvider.java @@ -1,3 +1,15 @@ +/* + * Copyright (c) 2010-2025 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.nikohomecontrol.internal; import java.io.ByteArrayInputStream; From 4c8b5dfa88a5bc28fecf4d33f853c2cc0b9f7a39 Mon Sep 17 00:00:00 2001 From: Mark Herwege Date: Tue, 14 Oct 2025 13:33:04 +0200 Subject: [PATCH 08/13] fix charger fields not poplulated Signed-off-by: Mark Herwege --- .../nhc2/NikoHomeControlCommunication2.java | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NikoHomeControlCommunication2.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NikoHomeControlCommunication2.java index 77deb226d7ae9..47e7383d0516d 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NikoHomeControlCommunication2.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NikoHomeControlCommunication2.java @@ -939,42 +939,41 @@ private void updateAlarmState(NhcAlarm2 alarmDevice, List devicePro } private void updateCarChargerState(NhcCarCharger2 carChargerDevice, List deviceProperties) { - String status = deviceProperties.stream().map(p -> p.status).filter(Objects::nonNull).findFirst().orElse(null); + Boolean status = deviceProperties.stream().map(p -> p.status).filter(Objects::nonNull).findFirst() + .map(s -> NHCON.equals(s)).orElse(carChargerDevice.getStatus()); String chargingStatus = deviceProperties.stream().map(p -> p.chargingStatus).filter(Objects::nonNull) - .findFirst().orElse(null); + .findFirst().orElse(carChargerDevice.getChargingStatus()); String evStatus = deviceProperties.stream().map(p -> p.evStatus).filter(Objects::nonNull).findFirst() .orElse(null); String couplingStatus = deviceProperties.stream().map(p -> p.couplingStatus).filter(Objects::nonNull) - .findFirst().orElse(null); + .findFirst().orElse(carChargerDevice.getCouplingStatus()); Integer electricalPower = deviceProperties.stream().map(p -> p.electricalPower) .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null) - .filter(Objects::nonNull).findFirst().orElse(null); - if (status != null || chargingStatus != null || evStatus != null || couplingStatus != null - || electricalPower != null) { - logger.debug( - "setting car charger device {} status to {}, charging status to {}, EV status to {}, coupling status to {}, electrical power to {}", - carChargerDevice.getId(), status, chargingStatus, evStatus, couplingStatus, electricalPower); - carChargerDevice.setStatus(NHCON.equals(status) ? true : false, chargingStatus, evStatus, couplingStatus, - electricalPower); - } + .filter(Objects::nonNull).findFirst().orElse(carChargerDevice.getElectricalPower()); + logger.debug( + "setting car charger device {} status to {}, charging status to {}, EV status to {}, coupling status to {}, electrical power to {}", + carChargerDevice.getId(), status, chargingStatus, evStatus, couplingStatus, electricalPower); + carChargerDevice.setStatus(status == null ? false : status, chargingStatus, evStatus, couplingStatus, + electricalPower); String chargingMode = deviceProperties.stream().map(p -> p.chargingMode).filter(Objects::nonNull).findFirst() - .orElse(null); + .orElse(carChargerDevice.getChargingMode()); Float targetDistance = deviceProperties.stream().map(p -> p.targetDistance).filter(Objects::nonNull).findFirst() .map(Float::parseFloat).orElse(null); String targetTime = deviceProperties.stream().map(p -> p.targetTime).filter(Objects::nonNull).findFirst() - .orElse(null); + .orElse(carChargerDevice.getTargetTime()); Boolean boost = deviceProperties.stream().map(p -> p.boost).filter(Objects::nonNull).findFirst() - .map(b -> NHCTRUE.equals(b) ? true : false).orElse(null); + .map(b -> NHCTRUE.equals(b) ? true : false).orElse(carChargerDevice.isBoost()); Float reachableDistance = deviceProperties.stream().map(p -> p.reachableDistance).filter(Objects::nonNull) - .findFirst().map(Float::parseFloat).orElse(null); + .findFirst().map(Float::parseFloat).orElse(carChargerDevice.getReachableDistance()); String nextChargingTime = deviceProperties.stream().map(p -> p.nextChargingTime).filter(Objects::nonNull) - .findFirst().orElse(null); - if (chargingMode != null || targetDistance != null || targetTime != null || boost != null - || reachableDistance != null || nextChargingTime != null) { - carChargerDevice.setChargingMode(chargingMode, targetDistance, targetTime, boost, reachableDistance, - nextChargingTime); - } + .findFirst().orElse(carChargerDevice.getNextChargingTime()); + logger.debug( + "setting car charger device {} charging mode to {}, target distance to {}, target time to {}, boost to {}, reachable distance to {}, next charging time to {}", + carChargerDevice.getId(), chargingMode, targetDistance, targetTime, boost, reachableDistance, + nextChargingTime); + carChargerDevice.setChargingMode(chargingMode, targetDistance, targetTime, boost, reachableDistance, + nextChargingTime); } @Override From 4355287ef65b3b534345e2f46f20a468beb90908 Mon Sep 17 00:00:00 2001 From: Mark Herwege Date: Tue, 14 Oct 2025 14:44:52 +0200 Subject: [PATCH 09/13] fix channel labels Signed-off-by: Mark Herwege --- .../src/main/resources/OH-INF/thing/thing-types.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml index 37734f54585ee..77b4e8a99d873 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml @@ -215,15 +215,15 @@ - @text/channelEnergyFromGridDescription + @text/channelEnergyFromGridDayDescription - @text/channelEnergyToGridDayDescription + @text/channelEnergyToGridDescription - - @text/channelEnergyToGridDescription + + @text/channelEnergyToGridDayDescription From 02c97abcb18fd972d07ba2a0abcd7a2cb6aa80e7 Mon Sep 17 00:00:00 2001 From: Mark Herwege Date: Tue, 14 Oct 2025 16:42:53 +0200 Subject: [PATCH 10/13] fix car charger power Signed-off-by: Mark Herwege --- .../NikoHomeControlCarChargerHandler.java | 6 ++-- .../internal/protocol/NhcCarCharger.java | 24 ++++++------- .../nhc2/NikoHomeControlCommunication2.java | 35 +++++++++++-------- .../resources/OH-INF/thing/thing-types.xml | 2 +- 4 files changed, 37 insertions(+), 30 deletions(-) diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlCarChargerHandler.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlCarChargerHandler.java index 68377523b9539..fdbf5de97f8e2 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlCarChargerHandler.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlCarChargerHandler.java @@ -115,7 +115,7 @@ void handleCommandSelection(ChannelUID channelUID, Command command) { switch (channelUID.getId()) { case CHANNEL_STATUS: if (command instanceof OnOffType onOffCommand) { - nhcCarCharger.executeCarChargerStatus(OnOffType.ON.equals(onOffCommand) ? true : false); + nhcCarCharger.executeCarChargerStatus(OnOffType.ON.equals(onOffCommand)); } break; case CHANNEL_CHARGING_MODE: @@ -152,10 +152,12 @@ void handleCommandSelection(ChannelUID channelUID, Command command) { break; case CHANNEL_BOOST: if (command instanceof OnOffType onOffCommand) { - nhcCarCharger.executeCarChargerChargingBoost(OnOffType.ON.equals(onOffCommand) ? true : false); + nhcCarCharger.executeCarChargerChargingBoost(OnOffType.ON.equals(onOffCommand)); } + break; default: logger.debug("unexpected command for channel {}", channelUID.getId()); + break; } } } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcCarCharger.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcCarCharger.java index 32f81f798211d..17bf60e98bc97 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcCarCharger.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcCarCharger.java @@ -263,13 +263,13 @@ public int getElectricalPower() { * @param couplingStatus the new coupling status, or {@code null} to keep the current value * @param electricalPower the new electrical power value, or {@code null} to keep the current value */ - public void setStatus(boolean status, @Nullable String chargingStatus, @Nullable String evStatus, + public void setStatus(@Nullable Boolean status, @Nullable String chargingStatus, @Nullable String evStatus, @Nullable String couplingStatus, @Nullable Integer electricalPower) { - this.status = status; - this.chargingStatus = chargingStatus != null ? chargingStatus : this.chargingStatus; - this.evStatus = evStatus != null ? evStatus : this.evStatus; - this.couplingStatus = couplingStatus != null ? couplingStatus : this.couplingStatus; - this.electricalPower = electricalPower != null ? electricalPower : this.electricalPower; + this.status = (status != null) ? status : this.status; + this.chargingStatus = (chargingStatus != null) ? chargingStatus : this.chargingStatus; + this.evStatus = (evStatus != null) ? evStatus : this.evStatus; + this.couplingStatus = (couplingStatus != null) ? couplingStatus : this.couplingStatus; + this.electricalPower = (electricalPower != null) ? electricalPower : this.electricalPower; NhcCarChargerEvent eventHandler = this.eventHandler; if (eventHandler != null) { @@ -324,12 +324,12 @@ public void setReading(double reading, double dayReading, LocalDateTime lastRead public void setChargingMode(@Nullable String chargingMode, @Nullable Float targetDistance, @Nullable String targetTime, @Nullable Boolean boost, @Nullable Float reachableDistance, @Nullable String nextChargingTime) { - this.chargingMode = chargingMode != null ? chargingMode : this.chargingMode; - this.targetDistance = targetDistance != null ? targetDistance : this.targetDistance; - this.targetTime = targetTime != null && !targetTime.isEmpty() ? targetTime : this.targetTime; - this.boost = boost != null ? boost : this.boost; - this.reachableDistance = reachableDistance != null ? reachableDistance : this.reachableDistance; - this.nextChargingTime = nextChargingTime != null && !nextChargingTime.isEmpty() ? nextChargingTime + this.chargingMode = (chargingMode != null) ? chargingMode : this.chargingMode; + this.targetDistance = (targetDistance != null) ? targetDistance : this.targetDistance; + this.targetTime = (targetTime != null && !targetTime.isEmpty()) ? targetTime : this.targetTime; + this.boost = (boost != null) ? boost : this.boost; + this.reachableDistance = (reachableDistance != null) ? reachableDistance : this.reachableDistance; + this.nextChargingTime = (nextChargingTime != null && !nextChargingTime.isEmpty()) ? nextChargingTime : this.nextChargingTime; NhcCarChargerEvent eventHandler = this.eventHandler; if (eventHandler != null) { diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NikoHomeControlCommunication2.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NikoHomeControlCommunication2.java index 47e7383d0516d..687b3bb6f0983 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NikoHomeControlCommunication2.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NikoHomeControlCommunication2.java @@ -940,21 +940,23 @@ private void updateAlarmState(NhcAlarm2 alarmDevice, List devicePro private void updateCarChargerState(NhcCarCharger2 carChargerDevice, List deviceProperties) { Boolean status = deviceProperties.stream().map(p -> p.status).filter(Objects::nonNull).findFirst() - .map(s -> NHCON.equals(s)).orElse(carChargerDevice.getStatus()); + .map(s -> NHCON.equals(s)).orElse(null); String chargingStatus = deviceProperties.stream().map(p -> p.chargingStatus).filter(Objects::nonNull) - .findFirst().orElse(carChargerDevice.getChargingStatus()); + .findFirst().orElse(null); String evStatus = deviceProperties.stream().map(p -> p.evStatus).filter(Objects::nonNull).findFirst() .orElse(null); String couplingStatus = deviceProperties.stream().map(p -> p.couplingStatus).filter(Objects::nonNull) - .findFirst().orElse(carChargerDevice.getCouplingStatus()); + .findFirst().orElse(null); Integer electricalPower = deviceProperties.stream().map(p -> p.electricalPower) .map(s -> (!((s == null) || s.isEmpty())) ? Math.round(Float.parseFloat(s)) : null) - .filter(Objects::nonNull).findFirst().orElse(carChargerDevice.getElectricalPower()); - logger.debug( - "setting car charger device {} status to {}, charging status to {}, EV status to {}, coupling status to {}, electrical power to {}", - carChargerDevice.getId(), status, chargingStatus, evStatus, couplingStatus, electricalPower); - carChargerDevice.setStatus(status == null ? false : status, chargingStatus, evStatus, couplingStatus, - electricalPower); + .filter(Objects::nonNull).findFirst().orElse(null); + if (status != null || chargingStatus != null || evStatus != null || couplingStatus != null + || electricalPower != null) { + logger.debug( + "setting car charger device {} status to {}, charging status to {}, EV status to {}, coupling status to {}, electrical power to {}", + carChargerDevice.getId(), status, chargingStatus, evStatus, couplingStatus, electricalPower); + carChargerDevice.setStatus(status, chargingStatus, evStatus, couplingStatus, electricalPower); + } String chargingMode = deviceProperties.stream().map(p -> p.chargingMode).filter(Objects::nonNull).findFirst() .orElse(carChargerDevice.getChargingMode()); @@ -968,12 +970,15 @@ private void updateCarChargerState(NhcCarCharger2 carChargerDevice, List p.nextChargingTime).filter(Objects::nonNull) .findFirst().orElse(carChargerDevice.getNextChargingTime()); - logger.debug( - "setting car charger device {} charging mode to {}, target distance to {}, target time to {}, boost to {}, reachable distance to {}, next charging time to {}", - carChargerDevice.getId(), chargingMode, targetDistance, targetTime, boost, reachableDistance, - nextChargingTime); - carChargerDevice.setChargingMode(chargingMode, targetDistance, targetTime, boost, reachableDistance, - nextChargingTime); + if (chargingMode != null || targetDistance != null || targetTime != null || boost != null + || reachableDistance != null || nextChargingTime != null) { + logger.debug( + "setting car charger device {} charging mode to {}, target distance to {}, target time to {}, boost to {}, reachable distance to {}, next charging time to {}", + carChargerDevice.getId(), chargingMode, targetDistance, targetTime, boost, reachableDistance, + nextChargingTime); + carChargerDevice.setChargingMode(chargingMode, targetDistance, targetTime, boost, reachableDistance, + nextChargingTime); + } } @Override diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml index 77b4e8a99d873..051de469889a0 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml @@ -495,7 +495,7 @@ - + From fdb3ad486dba8ec9f740570ba4505708498140e2 Mon Sep 17 00:00:00 2001 From: Mark Herwege Date: Thu, 16 Oct 2025 23:04:06 +0200 Subject: [PATCH 11/13] copilot review corrections Signed-off-by: Mark Herwege --- .../internal/handler/NikoHomeControlCarChargerHandler.java | 2 +- .../nikohomecontrol/internal/protocol/NhcMeterEvent.java | 4 ++-- .../src/main/resources/OH-INF/i18n/nikohomecontrol.properties | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlCarChargerHandler.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlCarChargerHandler.java index fdbf5de97f8e2..58ecb139c3fe1 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlCarChargerHandler.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlCarChargerHandler.java @@ -340,7 +340,7 @@ public void meterReadingEvent(double reading, double dayReading, LocalDateTime l NikoHomeControlBridgeHandler bridgeHandler = getBridgeHandler(); if (bridgeHandler == null) { - logger.debug("Cannot update char charger channels, no bridge handler"); + logger.debug("Cannot update car charger channels, no bridge handler"); return; } diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcMeterEvent.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcMeterEvent.java index 19681cb38ae41..3aff52986abb1 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcMeterEvent.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcMeterEvent.java @@ -45,7 +45,7 @@ public interface NhcMeterEvent extends NhcBaseEvent { * @param powerToGrid current power sent to grid in W, null for an empty reading */ default void meterPowerEvent(@Nullable Double power, @Nullable Double powerFromGrid, @Nullable Double powerToGrid) { - meterPowerEvent(power, powerFromGrid, powerToGrid); + meterPowerEvent(power); } /** @@ -66,7 +66,7 @@ default void meterPeakPowerFromGridEvent(double peakPowerFromGrid) { void meterReadingEvent(double reading, double dayReading, LocalDateTime lastReadingUTC); /** - * This method is called when meter readinsg are received from the Niko Home Control controller. + * This method is called when meter readings are received from the Niko Home Control controller. * This method should be used for meters that register multiple measurements at the same time. * The keys of the argument maps are the keys to the readings as received from the controller. * diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/i18n/nikohomecontrol.properties b/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/i18n/nikohomecontrol.properties index da51f1c5fd305..9b8e3aa385997 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/i18n/nikohomecontrol.properties +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/i18n/nikohomecontrol.properties @@ -248,7 +248,7 @@ offline.configuration-error.deviceRemoved = Device has been removed from control offline.configuration-error.actionType = Unsupported action type offline.configuration-error.meterType = Unsupported meter type -offline.configration-err.meterStartDate = Cannot parse meter start date configuration parameter +offline.configuration-error.meterStartDate = Cannot parse meter start date configuration parameter offline.configuration-error.invalid-bridge-handler = Invalid bridge handler From a1efd784326a845f34f7c6e5f84465684d281633 Mon Sep 17 00:00:00 2001 From: Mark Herwege Date: Fri, 17 Oct 2025 21:34:40 +0200 Subject: [PATCH 12/13] review adjustments Signed-off-by: Mark Herwege --- .../NikoHomeControlBindingConstants.java | 16 +++--- .../NikoHomeControlCarChargerHandler.java | 2 +- .../resources/OH-INF/thing/thing-types.xml | 49 +++++++++++++------ 3 files changed, 42 insertions(+), 25 deletions(-) diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/NikoHomeControlBindingConstants.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/NikoHomeControlBindingConstants.java index 542f7330cb222..2ba89d1ca7fcb 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/NikoHomeControlBindingConstants.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/NikoHomeControlBindingConstants.java @@ -113,15 +113,15 @@ public class NikoHomeControlBindingConstants { public static final String CHANNEL_STATE = "state"; public static final String CHANNEL_STATUS = "status"; - public static final String CHANNEL_CHARGING_STATUS = "chargingStatus"; - public static final String CHANNEL_EV_STATUS = "evStatus"; - public static final String CHANNEL_COUPLING_STATUS = "couplingStatus"; - public static final String CHANNEL_CHARGING_MODE = "chargingMode"; - public static final String CHANNEL_TARGET_DISTANCE = "targetDistance"; - public static final String CHANNEL_TARGET_TIME = "targetTime"; + public static final String CHANNEL_CHARGING_STATUS = "chargingstatus"; + public static final String CHANNEL_EV_STATUS = "evstatus"; + public static final String CHANNEL_COUPLING_STATUS = "couplingstatus"; + public static final String CHANNEL_CHARGING_MODE = "chargingmode"; + public static final String CHANNEL_TARGET_DISTANCE = "targetdistance"; + public static final String CHANNEL_TARGET_TIME = "targettime"; public static final String CHANNEL_BOOST = "boost"; - public static final String CHANNEL_REACHABLE_DISTANCE = "reachableDistance"; - public static final String CHANNEL_NEXT_CHARGING_TIME = "nextChargingTime"; + public static final String CHANNEL_REACHABLE_DISTANCE = "reachabledistance"; + public static final String CHANNEL_NEXT_CHARGING_TIME = "nextchargingtime"; public static final String CHANNEL_ALARM = "alarm"; public static final String CHANNEL_NOTICE = "notice"; diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlCarChargerHandler.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlCarChargerHandler.java index 58ecb139c3fe1..9f8dd0dea64b9 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlCarChargerHandler.java +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlCarChargerHandler.java @@ -179,7 +179,7 @@ public void initialize() { updateStatus(ThingStatus.UNKNOWN); Bridge bridge = getBridge(); - if ((bridge != null) && ThingStatus.ONLINE.equals(bridge.getStatus())) { + if (bridge != null && ThingStatus.ONLINE.equals(bridge.getStatus())) { // We need to do this in a separate thread because we may have to wait for the // communication to become active commStartThread = scheduler.submit(this::startCommunication); diff --git a/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml index 051de469889a0..710f6836f49e8 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/resources/OH-INF/thing/thing-types.xml @@ -492,16 +492,16 @@ @text/channelCarChargerStatusDescription - - - + + + - - - + + + - - + + @@ -786,7 +786,7 @@ - + String @text/channelChargingStatusDescription @@ -802,7 +802,7 @@ - + String @text/channelEvStatusDescription @@ -817,7 +817,7 @@ - + String @text/channelCouplingStatusDescription @@ -837,7 +837,7 @@ - + String @text/channelChargingModeDescription @@ -852,33 +852,50 @@ - + Number:Length @text/channelTargetDistanceDescription + + Setpoint + - + DateTime @text/channelTargetTimeDescription + + Setpoint + Timestamp + Switch @text/channelBoostDescription + + Switch + - + Number:Length @text/channelReachableDistanceDescription + + Calculation + - + DateTime @text/channelNextChargingTimeDescription + + Calculation + Timestamp + From 7615e0a8075022b2183e1d05f62fe89dc56a0ddc Mon Sep 17 00:00:00 2001 From: Mark Herwege Date: Sun, 19 Oct 2025 18:21:31 +0200 Subject: [PATCH 13/13] update README Signed-off-by: Mark Herwege --- .../README.md | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/bundles/org.openhab.binding.nikohomecontrol/README.md b/bundles/org.openhab.binding.nikohomecontrol/README.md index 3fdfc7c355b95..bc923a502372a 100644 --- a/bundles/org.openhab.binding.nikohomecontrol/README.md +++ b/bundles/org.openhab.binding.nikohomecontrol/README.md @@ -209,9 +209,9 @@ Thing nikohomecontrol:carCharger:mybridge:mycarcharger [ carChargerId="abcdef01- | heatingdemand | R | | String | thermostat | indicating if the system is actively heating/cooling. This channel will have value Heating, Cooling or None. For NHC I this is set by the binding from the temperature difference between `setpoint` and `measured`. It therefore may incorrectly indicate cooling even when the system does not have active cooling capabilities | | demand | R | X | Number | thermostat | indicating if the system is actively heating/cooling, same as `heatingdemand` but numeric values (-1=Cooling, 0=None, 1=Heating) | | power | R | | Number:Power | energyMeterHome, energyMeterLive | instant power consumption/production (negative for production), refreshed every 2s. Linking this channel starts an intensive communication flow with the controller and should only be done when appropriate | -| powerFromGrid | R | | Number:Power | energyMeterHome | power consumption grid for home energy meter, refreshed every 2s. Linking this channel starts an intensive communication flow with the controller and should only be done when appropriate | -| powerToGrid | R | | Number:Power | energyMeterHome | power sent to grid for home energy meter, refreshed every 2s. Linking this channel starts an intensive communication flow with the controller and should only be done when appropriate | -| peakPowerFromGrid | R | | Number:Power | energyMeterHome | current month peak power as registered by the home energy meter | +| powerfromgrid | R | | Number:Power | energyMeterHome | power consumption grid for home energy meter, refreshed every 2s. Linking this channel starts an intensive communication flow with the controller and should only be done when appropriate | +| powertogrid | R | | Number:Power | energyMeterHome | power sent to grid for home energy meter, refreshed every 2s. Linking this channel starts an intensive communication flow with the controller and should only be done when appropriate | +| peakpowerfromgrid | R | | Number:Power | energyMeterHome | current month peak power as registered by the home energy meter | | energy | R | | Number:Energy | energyMeterHome, energyMeterLive, energyMeter, carCharger | total energy meter reading | | energyday | R | | Number:Energy | energyMeterHome, energyMeterLive, energyMeter, carCharger | day energy meter reading | | gas | R | | Number:Volume | energyMeterHome, gasMeter | total gas meter reading | @@ -226,16 +226,16 @@ Thing nikohomecontrol:carCharger:mybridge:mycarcharger [ carChargerId="abcdef01- | armed | RW | | Switch | alarm | state of the alarm system (on/off), will only turn on after pre-armed period when arming | | state | R | | String | alarm | state of the alarm system (DISARMED, PREARMED, ARMED, PREALARM, ALARM, DETECTOR PROBLEM) | | status | RW | | Switch | carCharger | status of the car charger (on/off) | -| chargingStatus | R | | String | carCharger | charging status of the car charger (ACTIVE, INACTIVE, BATTERY FULL or ERROR) | -| evStatus | R | | String | carCharger | status of the electric vehicle (IDLE, CONNECTED or CHARGING) | -| couplingStatus | R | | String | carCharger | coupling status (OK, NO INTERNET, NO CREDENTIALS, INVALID CREDENTIALS, CONNECTION ERROR, CONNECTION TIMEOUT, API ERROR or UNKNOWN ERROR) | -| electricalPower | R | | Number:Power | carCharger | current charging power | -| chargingMode | RW | | String | carCharger | charging mode (SOLAR, NORMAL or SMART) | -| targetDistance | RW | | Number:Length | carCharger | target distance to achieve in charging activity | -| targetTime | RW | | DateTime | carCharger | time by which the target distance should be achieved | +| chargingstatus | R | | String | carCharger | charging status of the car charger (ACTIVE, INACTIVE, BATTERY FULL or ERROR) | +| evstatus | R | | String | carCharger | status of the electric vehicle (IDLE, CONNECTED or CHARGING) | +| couplingstatus | R | | String | carCharger | coupling status (OK, NO INTERNET, NO CREDENTIALS, INVALID CREDENTIALS, CONNECTION ERROR, CONNECTION TIMEOUT, API ERROR or UNKNOWN ERROR) | +| electricalpower | R | | Number:Power | carCharger | current charging power | +| chargingmode | RW | | String | carCharger | charging mode (SOLAR, NORMAL or SMART) | +| targetdistance | RW | | Number:Length | carCharger | target distance to achieve in charging activity | +| targettime | RW | | DateTime | carCharger | time by which the target distance should be achieved | | boost | RW | | Switch | carCharger | boost charging to maximum achievable, not respecting capacity limit | -| reachableDistance | R | | Number:Length | carCharger | reachable distance in current charing activity | -| nextChargingTime | R | | DateTime | carCharger | next charging start in current charging activity | +| reachabledistance | R | | Number:Length | carCharger | reachable distance in current charing activity | +| nextchargingtime | R | | DateTime | carCharger | next charging start in current charging activity | | alarm | | | | bridge, alarm | trigger channel with alarm event message, can be used in rules | | notice | | | | bridge | trigger channel with notice event message, can be used in rules | @@ -250,7 +250,8 @@ To help with further development, a number of console commands allow you to coll ## Limitations -The binding has been tested with a Niko Home Control I IP-interface (550-00508) and the Niko Home Control Connected Controller (550-00003) for Niko Home Control I and II, and the Niko Home Control Wireless Smart Hub for Niko Home Control II. +The binding has been tested with a Niko Home Control I IP-interface (550-00508) and the Niko Home Control Connected Controller (550-00003) for Niko Home Control I and II, and the Niko Home Control Wireless Smart Hub (552-00001) for Niko Home Control II. +Also the Wireless Bridge (550-00640) for the Connected Controller is supported. Not all action and device types supported in Niko Home Control I and II controllers are supported by the binding. Refer to the list of things and their support for Niko Home Control I and II respectively.