diff --git a/bundles/org.openhab.binding.nikohomecontrol/README.md b/bundles/org.openhab.binding.nikohomecontrol/README.md index 59dbafecb8d86..bc923a502372a 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, 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,13 +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 | car charger device | ## Binding Configuration @@ -128,7 +130,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 | |---------------|:-----:|:------:|:--------:|----------------------------------|-----------------------------------------------------------------------------------| @@ -137,10 +139,11 @@ 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 | +| 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 +154,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,37 +187,57 @@ 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 | -|-----------------|:--:|:--------:|--------------------|-------------|-----------------------------------------------------------------------------------------------------| -| 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) | -| 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 | +| 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 | 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, 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, 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 | +| 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 | ## Console Commands @@ -230,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. 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..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 @@ -47,12 +47,14 @@ 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"); 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); @@ -60,14 +62,14 @@ 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); - 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 @@ -85,15 +87,22 @@ 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_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_LAST = "energylast"; - public static final String CHANNEL_GAS_LAST = "gaslast"; - public static final String CHANNEL_WATER_LAST = "waterlast"; + 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_MEASUREMENT_TIME = "measurementtime"; public static final String CHANNEL_BELL_BUTTON = "bellbutton"; public static final String CHANNEL_RING_AND_COME_IN = "ringandcomein"; @@ -103,6 +112,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"; @@ -124,11 +144,14 @@ 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"; 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..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 @@ -21,9 +21,11 @@ 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; +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; @@ -40,6 +42,7 @@ * handlers. * * @author Mark Herwege - Initial Contribution + * @author Mark Herwege - Add car chargers */ @NonNullByDefault @@ -48,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 @@ -65,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); } @@ -79,6 +85,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/SslContextProvider.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/SslContextProvider.java new file mode 100644 index 0000000000000..98fb84042e84b --- /dev/null +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/SslContextProvider.java @@ -0,0 +1,123 @@ +/* + * 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; +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/discovery/NikoHomeControlDiscoveryService.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/discovery/NikoHomeControlDiscoveryService.java index 24cc86a6976d3..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 @@ -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, @@ -143,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); @@ -198,6 +205,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/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..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 @@ -23,8 +23,13 @@ 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; import org.openhab.core.net.NetworkAddressService; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ThingStatus; @@ -41,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 { @@ -49,9 +55,13 @@ public class NikoHomeControlBridgeHandler2 extends NikoHomeControlBridgeHandler private final Gson gson = new GsonBuilder().create(); + private HttpClientFactory httpClientFactory; + private @Nullable HttpClient httpClient; + public NikoHomeControlBridgeHandler2(Bridge nikoHomeControlBridge, NetworkAddressService networkAddressService, - TimeZoneProvider timeZoneProvider) { + TimeZoneProvider timeZoneProvider, HttpClientFactory httpClientFactory) { super(nikoHomeControlBridge, networkAddressService, timeZoneProvider); + this.httpClientFactory = httpClientFactory; } @Override @@ -85,7 +95,13 @@ public void initialize() { addr = (addr == null) ? "unknown" : addr.replace(".", "_"); String clientId = addr + "-" + thing.getUID().toString().replace(":", "_"); try { - nhcComm = new NikoHomeControlCommunication2(this, clientId, scheduler); + 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) { // this should not happen unless there is a programming error @@ -95,6 +111,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()); 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..7c8519cbf6d4f --- /dev/null +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlCarChargerConfig.java @@ -0,0 +1,28 @@ +/* + * 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 = ""; + 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 new file mode 100644 index 0000000000000..9f8dd0dea64b9 --- /dev/null +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/handler/NikoHomeControlCarChargerHandler.java @@ -0,0 +1,358 @@ +/* + * 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.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; + +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.config.core.Configuration; +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; + 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()) { + case CHANNEL_STATUS: + if (command instanceof OnOffType onOffCommand) { + nhcCarCharger.executeCarChargerStatus(OnOffType.ON.equals(onOffCommand)); + } + 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)); + } + break; + default: + logger.debug("unexpected command for channel {}", channelUID.getId()); + break; + } + } + } + + @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; + } + + 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); + + 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()); + } + 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) { + nhcComm.stopMeter(deviceId); + NhcCarCharger carCharger = nhcComm.getCarChargerDevices().get(deviceId); + if (carCharger != null) { + carCharger.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); + } + + @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 car 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/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 5a5aabb0b52fc..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 @@ -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; @@ -50,6 +56,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 { @@ -60,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); } @@ -83,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); @@ -143,13 +150,35 @@ 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; } + 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); @@ -167,12 +196,20 @@ 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)) { + if (isLinked(CHANNEL_POWER) || isLinked(CHANNEL_POWER_TO_GRID) || isLinked(CHANNEL_POWER_FROM_GRID)) { nhcComm.startMeterLive(deviceId); } } @@ -212,7 +249,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 Double power, @Nullable Double powerFromGrid, @Nullable Double powerToGrid) { NhcMeter nhcMeter = this.nhcMeter; if (nhcMeter == null) { logger.debug("meter with ID {} not initialized", deviceId); @@ -220,7 +262,7 @@ public void meterPowerEvent(@Nullable Integer power) { } 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; } @@ -229,9 +271,31 @@ public void meterPowerEvent(@Nullable Integer power) { 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) { + 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(double 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); } @@ -261,19 +325,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: @@ -281,15 +345,91 @@ 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; + } + + ZonedDateTime lastReading = lastReadingUTC.atZone(ZoneOffset.UTC) + .withZoneSameInstant(bridgeHandler.getTimeZone()); + + boolean invert = getConfig().as(NikoHomeControlMeterConfig.class).invert; + meterReadings.forEach((reading, v) -> { + 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<>(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<>(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<>(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<>(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<>(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<>(dayValue, SIUnits.CUBIC_METRE)); + } + break; + default: + break; + } + }); + + if (!meterReadings.isEmpty()) { + updateState(CHANNEL_MEASUREMENT_TIME, new DateTimeType(lastReading)); + updateStatus(ThingStatus.ONLINE); + } + } + @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 - 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()); - 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()) { @@ -301,15 +441,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()) { @@ -321,6 +465,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/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 new file mode 100644 index 0000000000000..17bf60e98bc97 --- /dev/null +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcCarCharger.java @@ -0,0 +1,437 @@ +/* + * 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 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; +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; + 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; + + private Random r = new Random(); + + 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; + } + + /** + * 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(@Nullable Boolean status, @Nullable String chargingStatus, @Nullable String evStatus, + @Nullable String couplingStatus, @Nullable Integer 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) { + 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); + } + } + + /** + * @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. + * 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); + } + } + + /** + * 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 carChargerRemoved() { + 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); + } + + /** + * 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 new file mode 100644 index 0000000000000..a070af6536d38 --- /dev/null +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcCarChargerEvent.java @@ -0,0 +1,66 @@ +/* + * 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 java.time.LocalDateTime; + +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); + + /** + * 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/NhcMeter.java b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/NhcMeter.java index 376699ea03e6c..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; @@ -29,6 +30,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 { @@ -44,12 +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 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; @@ -75,10 +78,26 @@ protected NhcMeter(String id, String name, MeterType type, @Nullable LocalDateTi */ protected void updatePowerState() { NhcMeterEvent handler = eventHandler; - Integer value = getPower(); - if ((handler != null) && (value != null)) { - logger.debug("update power channel for {} with {}", id, value); - handler.meterPowerEvent(value); + Double power = getPower(); + Double powerFromGrid = getPowerFromGrid(); + Double powerToGrid = getPowerToGrid(); + if ((handler != null) && (power != null)) { + logger.debug("update power channels for {} with {}, {}, {}", id, power, powerFromGrid, powerToGrid); + handler.meterPowerEvent(power, powerFromGrid, powerToGrid); + } + } + + /** + * 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; + Double value = getPeakPowerFromGrid(); + if (handler != null && value != null) { + logger.debug("update peakPowerFromGrid channel for {} with {}", id, value); + handler.meterPeakPowerFromGridEvent(value); } } @@ -183,51 +202,103 @@ 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 Double getPowerFromGrid() { + return powerFromGrid; + } + + /** + * @return the power in W (positive for consumption, negative for production), return null if no reading received + * yet + */ + public @Nullable Double 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) { + public void setPower(@Nullable Double 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 Double power, @Nullable Double powerFromGrid, @Nullable Double powerToGrid) { this.power = power; + this.powerFromGrid = powerFromGrid; + this.powerToGrid = powerToGrid; updatePowerState(); } + /** + * @return the peak power for the current month in W + */ + public @Nullable Double getPeakPowerFromGrid() { + return peakPowerFromGrid; + } + + /** + * @param peakPowerFromGrid the power to set in W + */ + 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; } /** @@ -242,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) { } /** @@ -280,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 652e53eb76337..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 @@ -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,27 @@ 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 + * 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 Double power, @Nullable Double powerFromGrid, @Nullable Double 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(double peakPowerFromGrid) { + } /** * This method is called when a meter reading is received from the Niko Home Control controller. @@ -43,4 +64,15 @@ public interface NhcMeterEvent extends NhcBaseEvent { * @param lastReadingUTC last meter reading date and time, UTC */ void meterReadingEvent(double reading, double dayReading, LocalDateTime lastReadingUTC); + + /** + * 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. + * + * @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 0f227a3a56d46..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 @@ -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. * @@ -233,8 +244,9 @@ public Map getAlarmDevices() { * 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 @@ -276,10 +288,16 @@ 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); + return; + } + NhcCarCharger carCharger = getCarChargerDevices().get(meterId); + if (carCharger != null) { + carCharger.startMeter(refresh, startDate); + return; } } @@ -290,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; } } @@ -301,6 +325,9 @@ public void stopAllMeters() { stopMeter(meterId); stopMeterLive(meterId); } + for (String carChargerId : getCarChargerDevices().keySet()) { + stopMeter(carChargerId); + } } /** @@ -353,4 +380,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..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 @@ -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"; @@ -86,4 +90,49 @@ 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"); + + // 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/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..afcf2a6e0d33a --- /dev/null +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcCarCharger2.java @@ -0,0 +1,64 @@ +/* + * 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.concurrent.ScheduledExecutorService; + +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, ScheduledExecutorService scheduler) { + super(id, name, location, nhcComm, scheduler); + 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..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 @@ -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,19 +70,21 @@ static class NhcProperty { String thermostatOn; @Nullable String hvacOn; + // fields for fans and ventilation @Nullable String fanSpeed; + // fields for electricity metering @Nullable - String electricalEnergy; - @Nullable String electricalPower; @Nullable String electricalPowerToGrid; @Nullable String electricalPowerFromGrid; @Nullable + String electricalMonthlyPeakPowerFromGrid; + @Nullable String electricalPowerProduction; @Nullable String electricalPowerSelfConsumption; @@ -89,9 +94,25 @@ 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 String doorlock; + // fields for video devices @Nullable String ipAddress; @@ -103,6 +124,7 @@ static class NhcProperty { String callStatus03; @Nullable String callStatus04; + // fields for alarms @Nullable String internalState; @@ -112,16 +134,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 +178,7 @@ static class NhcParameter { String locationName; @Nullable String locationIcon; + // fields for electricity metering @Nullable String flow; @@ -140,6 +188,7 @@ static class NhcParameter { String clampType; @Nullable String shortName; + // fields for access control @Nullable String buttonId; @@ -149,6 +198,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/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..63983a9b91275 --- /dev/null +++ b/bundles/org.openhab.binding.nikohomecontrol/src/main/java/org/openhab/binding/nikohomecontrol/internal/protocol/nhc2/NhcHttpConnection2.java @@ -0,0 +1,115 @@ +/* + * 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.HttpClientInitializationException; +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) + throws HttpClientInitializationException { + this.httpClient = httpClient; + this.token = token; + this.hostname = cocoAddress; + 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 = 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 { + 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, e); + 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..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 @@ -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,15 @@ @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 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 +74,46 @@ 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.readings = readings; + this.dayReadings = dayReadings; + 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/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 67db2d3cb3462..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 @@ -17,8 +17,14 @@ import java.lang.reflect.Type; 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; +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,11 +37,16 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; +import javax.measure.Unit; + 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; +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; @@ -48,10 +59,14 @@ 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.SIUnits; +import org.openhab.core.library.unit.Units; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -72,6 +87,8 @@ * * * @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 @@ -80,6 +97,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<>(); @@ -104,9 +123,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 @@ -133,12 +153,22 @@ public synchronized void startCommunication() { return; } + 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; } } @@ -375,6 +405,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) { @@ -385,15 +419,17 @@ 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); } else { logger.debug("device type {} and model {} not supported for {}, {}", device.type, device.model, device.uuid, device.name); @@ -430,6 +466,8 @@ private void addActionDevice(NhcDevice2 device, @Nullable String location) { case "sunblind": case "venetianblind": case "gate": + case "reynaers": + case "velux": actionType = ActionType.ROLLERSHUTTER; break; default: @@ -472,8 +510,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); } @@ -587,6 +636,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, scheduler); + } + carChargerDevices.put(device.uuid, nhcCarCharger); + } + private void removeDevice(NhcDevice2 device) { NhcAction action = actions.get(device.uuid); NhcThermostat thermostat = thermostats.get(device.uuid); @@ -594,6 +656,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 +675,9 @@ private void removeDevice(NhcDevice2 device) { } else if (alarm != null) { alarm.alarmDeviceRemoved(); alarmDevices.remove(device.uuid); + } else if (carCharger != null) { + carCharger.carChargerRemoved(); + carChargerDevices.remove(device.uuid); } } @@ -628,6 +694,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 +708,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); } @@ -703,22 +772,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) @@ -729,18 +798,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; } @@ -767,21 +836,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)); - logger.trace("setting energy meter {} power to {}", meter.getId(), power); - meter.setPower(power); + 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, powerToGrid); + meter.setPower(power, powerFromGrid, powerToGrid); } catch (NumberFormatException e) { logger.trace("wrong format in energy meter {} power reading", meter.getId()); - meter.setPower(null); + meter.setPower(null, null, null); + } + + try { + 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 {}", meter.getId(), peakPowerFromGrid); + meter.setPeakPowerFromGrid(peakPowerFromGrid); + } + } catch (NumberFormatException e) { + logger.trace("wrong format in energy meter {} peakPowerFromGrid reading", meter.getId()); + meter.setPeakPowerFromGrid(null); } } @@ -799,12 +885,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; @@ -852,6 +938,49 @@ 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(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(status, chargingStatus, evStatus, couplingStatus, electricalPower); + } + + String chargingMode = deviceProperties.stream().map(p -> p.chargingMode).filter(Objects::nonNull).findFirst() + .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(carChargerDevice.getTargetTime()); + Boolean boost = deviceProperties.stream().map(p -> p.boost).filter(Objects::nonNull).findFirst() + .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(carChargerDevice.getReachableDistance()); + String nextChargingTime = deviceProperties.stream().map(p -> p.nextChargingTime).filter(Objects::nonNull) + .findFirst().orElse(carChargerDevice.getNextChargingTime()); + 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 public void executeAction(String actionId, String value) { NhcMessage2 message = new NhcMessage2(); @@ -1000,8 +1129,86 @@ 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); + NhcCarCharger carCharger = carChargerDevices.get(meterId); + if (meter == null && carCharger == null) { + return; + } + + NhcHttpConnection2 httpConnection = this.httpConnection; + if (httpConnection == null) { + return; + } + ZonedDateTime nowUTC = ZonedDateTime.now().withZoneSameInstant(ZoneOffset.UTC); + LocalDateTime now = nowUTC.withZoneSameInstant(getTimeZone()).toLocalDateTime(); + LocalDateTime meterStart = LocalDateTime.parse(startDate); + LocalDateTime dayStart = now.truncatedTo(ChronoUnit.DAYS); + + 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(); + + 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) { + 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 && !unit.isEmpty()) { + try { + Unit receivedUnit = Units.getInstance().getUnit(unit); + 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()); + } + } + readings.put(property, value); + } + }); + + } catch (JsonSyntaxException e) { + logger.debug("unexpected json {}", response); + } + + return readings; } @Override @@ -1198,6 +1405,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..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 @@ -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 @@ -64,6 +67,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 @@ -84,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 @@ -119,12 +129,30 @@ 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) +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 @@ -163,6 +191,46 @@ 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 +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 @@ -180,6 +248,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.configuration-error.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 3c35a1a3ca6dd..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 @@ -186,6 +186,78 @@ + + + + + + @text/energyMeterHomeDescription + ElectricMeter + + + + + @text/channelPowerFromGridDescription + + + + @text/channelPowerToGridDescription + + + + @text/channelPeakPowerFromGridDescription + + + + + + @text/channelEnergyFromGridDescription + + + + @text/channelEnergyFromGridDayDescription + + + + @text/channelEnergyToGridDescription + + + + @text/channelEnergyToGridDayDescription + + + + @text/channelEnergySelfConsumptionDescription + + + + @text/channelEnergySelfConsumptionDayDescription + + + + + + + + + + + @text/deviceConfigDeviceIdDescription + + + + @text/meterConfigRefreshDescription + 10 + true + + + datetime + + @text/meterConfigStartDateDescription + true + + + @@ -198,8 +270,11 @@ - + + + 1 + @@ -211,6 +286,12 @@ 10 true + + datetime + + @text/meterConfigStartDateDescription + true + @text/meterConfigInvertDescription @@ -222,6 +303,7 @@ + @text/energyMeterDescription @@ -229,8 +311,11 @@ - + + + 1 + @@ -242,6 +327,12 @@ 10 true + + datetime + + @text/meterConfigStartDateDescription + true + @text/meterConfigInvertDescription @@ -253,6 +344,7 @@ + @text/gasMeterDescription @@ -260,8 +352,11 @@ - + + + 1 + @@ -273,6 +368,12 @@ 10 true + + datetime + + @text/meterConfigStartDateDescription + true + @text/meterConfigInvertDescription @@ -284,6 +385,7 @@ + @text/waterMeterDescription @@ -291,8 +393,11 @@ - + + + 1 + @@ -304,6 +409,12 @@ 10 true + + datetime + + @text/meterConfigStartDateDescription + true + @text/meterConfigInvertDescription @@ -369,6 +480,57 @@ + + + + + + @text/carChargerDescription + EVSE + + + + @text/channelCarChargerStatusDescription + + + + + + + + + + + + + + + + + + + @text/deviceConfigDeviceIdDescription + + + + @text/meterConfigRefreshDescription + 10 + true + + + datetime + + @text/meterConfigStartDateDescription + true + + + + @text/meterConfigInvertDescription + false + true + + + Switch @@ -624,6 +786,119 @@ + + String + + @text/channelChargingStatusDescription + + Status + + + + + + + + + + + + String + + @text/channelEvStatusDescription + + Status + + + + + + + + + + + String + + @text/channelCouplingStatusDescription + + Status + + + + + + + + + + + + + + + + String + + @text/channelChargingModeDescription + + Control + + + + + + + + + + + Number:Length + + @text/channelTargetDistanceDescription + + Setpoint + + + + + DateTime + + @text/channelTargetTimeDescription + + Setpoint + Timestamp + + + + + Switch + + @text/channelBoostDescription + + Switch + + + + Number:Length + + @text/channelReachableDistanceDescription + + Calculation + + + + + DateTime + + @text/channelNextChargingTimeDescription + + Calculation + Timestamp + + + + trigger 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 + + + + +