From e0967d799385605daffa5de55972704fec0c6895 Mon Sep 17 00:00:00 2001
From: jlaur <jacob-github@vindvejr.dk>
Date: Fri, 22 Oct 2021 20:10:41 +0200
Subject: [PATCH] [danfossairunit] Add filter period channel (#11371)

* Add filter period channel.

Fixes #11310

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>

* Round remaining filter life percentage to one decimal.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>

* Improve and extend example configuration.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>

* Mark filter period channel as advanced due to Link CC/Air Dial conflicts.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>

* Add comment about value getting overwritten.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>

* Remove redundant parentheses.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>

* Fix SCA report: First javadoc author should have 'Initial contribution' contribution description.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>

* Fix SCA issue: NoEmptyLineSeparatorCheck

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
---
 .../README.md                                 | 51 ++++++++++++++-----
 .../danfossairunit/internal/Channel.java      |  4 +-
 .../danfossairunit/internal/Commands.java     |  1 +
 .../internal/CommunicationController.java     |  2 +-
 .../internal/DanfossAirUnit.java              | 19 ++++++-
 .../DanfossAirUnitDiscoveryService.java       |  2 -
 .../resources/OH-INF/thing/thing-types.xml    |  7 +++
 .../internal/DanfossAirUnitTest.java          | 11 +++-
 8 files changed, 79 insertions(+), 18 deletions(-)

diff --git a/bundles/org.openhab.binding.danfossairunit/README.md b/bundles/org.openhab.binding.danfossairunit/README.md
index 669d08e2827dd..d6833f916c7d7 100644
--- a/bundles/org.openhab.binding.danfossairunit/README.md
+++ b/bundles/org.openhab.binding.danfossairunit/README.md
@@ -41,6 +41,7 @@ These are the available configuration parameters:
 | exhaust_temp | recuperator | Number | RO | Temperature of the air when pushed outside  |
 | battery_life | service | Number | RO | Remaining Air Dial Battery Level (percentage) |
 | filter_life | service | Number | RO | Remaining life of filter until exchange is necessary (percentage) |
+| filter_period | service | Number | RW | Number of months between filter replacements (between 3 and 12). This value affects calculation of filter_life by the unit, and might get overwritten by Air Dial or Link CC Controller. |
 
 
 ## Full Example
@@ -63,21 +64,47 @@ updateUnchangedValuesEveryMillis=30000]
 ### Items
 
 ```
-Dimmer Lueftung_Drehzahl_Manuell "Drehzahl Lüftung %" (All,Lueftung) {channel = "danfossairunit:airunit:myairunit:main#manual_fan_speed"}
-Number Lueftung_Drehzahl_Supply "Drehzahl Lüftung Zuluft (rpm)" (All,Lueftung) {channel = "danfossairunit:airunit:myairunit:main#supply_fan_speed"}
-Number Lueftung_Drehzahl_Extract "Drehzahl Lüftung Abluft (rpm)" (All,Lueftung) {channel = "danfossairunit:airunit:myairunit:main#extract_fan_speed"}
-String Lueftung_Mode "Betriebsart Lüftung" (All,Lueftung) {channel = "danfossairunit:airunit:myairunit:main#mode"}
-Switch Lueftung_Boost "Stoßlüftung" (All,Lueftung) {channel = "danfossairunit:airunit:myairunit:main#boost"}
-Switch Lueftung_Bypass "Lüftung Bypass" (All,Lueftung) {channel = "danfossairunit:airunit:myairunit:recuperator#bypass"}
+Dimmer DanfossHRV_ManualFanStep "Manual Fan Step [%s]" {channel = "danfossairunit:airunit:myairunit:main#manual_fan_step"}
+Number DanfossHRV_SupplyFanSpeed "Supply Fan Speed" {channel = "danfossairunit:airunit:myairunit:main#supply_fan_speed"}
+Number DanfossHRV_ExtractFanSpeed "Extract Fan Speed" {channel = "danfossairunit:airunit:myairunit:main#extract_fan_speed"}
+String DanfossHRV_Mode "Operation Mode" {channel = "danfossairunit:airunit:myairunit:main#mode"}
+Switch DanfossHRV_Boost "Boost" {channel = "danfossairunit:airunit:myairunit:main#boost"}
+Switch DanfossHRV_Bypass "Bypass" {channel = "danfossairunit:airunit:myairunit:recuperator#bypass"}
+Number:Dimensionless DanfossHRV_Humidity "Relative humidity" <humidity> { channel = "danfossairunit:airunit:myairunit:humidity#humidity" }
+Number:Temperature DanfossHRV_RoomTemperature "Room air temperatuyre" <temperature> { channel = "danfossairunit:airunit:myairunit:temps#room_temp" }
+Number:Temperature DanfossHRV_OutdoorTemperature "Outdoor air temperature" <temperature> { channel = "danfossairunit:airunit:myairunit:temps#outdoor_temp" }
+Number:Temperature DanfossHRV_SupplyAirTemperature "Supply air temperature" <temperature> { channel = "danfossairunit:airunit:myairunit:recuperator#supply_temp" }
+Number:Temperature DanfossHRV_ExtractAirTemperature "Extract air temperature" <temperature> { channel = "danfossairunit:airunit:myairunit:recuperator#extract_temp" }
+Number:Temperature DanfossHRV_ExhaustAirTemperature "Exhaust air temperature" <temperature> { channel = "danfossairunit:airunit:myairunit:recuperator#exhaust_temp" }
+Number DanfossHRV_RemainingFilterLife "Remaining filter life" { channel = "danfossairunit:airunit:myairunit:service#filter_life" }
+Number DanfossHRV_FilterPeriod "Filter period" { channel = "danfossairunit:airunit:myairunit:service#filter_period" }
 ```
 
 ### Sitemap
 
 ```
-Slider item=Lueftung_Drehzahl_Manuell
-Text item=Lueftung_Drehzahl_Supply
-Text item=Lueftung_Drehzahl_Extract
-Selection item=Lueftung_Mode mappings=[DEMAND="Bedarfslüftung", OFF="Aus", PROGRAM="Programm", MANUAL="manuell"]
-Switch item=Lueftung_Boost
-Switch item=Lueftung_Bypass
+sitemap danfoss label="Danfoss" {
+    Frame label="Control" {
+        Selection item=DanfossHRV_Mode mappings=[DEMAND="Demand", OFF="Off", PROGRAM="Program", MANUAL="Manual"]
+        Slider item=DanfossHRV_ManualFanStep step=10 visibility=[DanfossHRV_Mode=="MANUAL"]
+        Switch item=DanfossHRV_Bypass
+        Switch item=DanfossHRV_Boost
+    }
+    Frame label="Measurements" {
+        Text item=DanfossHRV_Humidity
+        Text item=DanfossHRV_RoomTemperature
+        Text item=DanfossHRV_OutdoorTemperature
+        Text item=DanfossHRV_SupplyAirTemperature
+        Text item=DanfossHRV_ExtractAirTemperature
+        Text item=DanfossHRV_ExhaustAirTemperature
+    }
+    Frame label="Fan" {
+        Text item=DanfossHRV_SupplyFanSpeed
+        Text item=DanfossHRV_ExtractFanSpeed
+    }
+    Frame label="Filter" {
+         Text item=DanfossHRV_RemainingFilterLife
+         Slider item=DanfossHRV_FilterPeriod minValue=3 maxValue=12
+    }
+}
 ```
diff --git a/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/Channel.java b/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/Channel.java
index ea77d6baf9e26..4aaeeb2b7c6b5 100644
--- a/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/Channel.java
+++ b/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/Channel.java
@@ -59,7 +59,9 @@ public enum Channel {
 
     // service channels
     CHANNEL_BATTERY_LIFE("battery_life", ChannelGroup.SERVICE, DanfossAirUnit::getBatteryLife),
-    CHANNEL_FILTER_LIFE("filter_life", ChannelGroup.SERVICE, DanfossAirUnit::getFilterLife);
+    CHANNEL_FILTER_LIFE("filter_life", ChannelGroup.SERVICE, DanfossAirUnit::getFilterLife),
+    CHANNEL_FILTER_PERIOD("filter_period", ChannelGroup.SERVICE, DanfossAirUnit::getFilterPeriod,
+            DanfossAirUnit::setFilterPeriod);
 
     private final String channelName;
     private final ChannelGroup group;
diff --git a/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/Commands.java b/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/Commands.java
index ba98872a2afb8..752458d031993 100644
--- a/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/Commands.java
+++ b/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/Commands.java
@@ -57,6 +57,7 @@ public class Commands {
     public static byte[] EXHAUST_TEMPERATURE = { 0x14, 0x75 };
     public static byte[] BATTERY_LIFE = { 0x03, 0x0f };
     public static byte[] FILTER_LIFE = { 0x14, 0x6a };
+    public static byte[] FILTER_PERIOD = { 0x14, 0x69 };
     public static byte[] CURRENT_TIME = { 0x15, (byte) 0xe0 };
     public static byte[] AWAY_TO = { 0x15, 0x20 };
     public static byte[] AWAY_FROM = { 0x15, 0x21 };
diff --git a/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/CommunicationController.java b/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/CommunicationController.java
index 6d0873762a1d4..b240b89bcbc7a 100644
--- a/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/CommunicationController.java
+++ b/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/CommunicationController.java
@@ -19,7 +19,7 @@
 /**
  * This interface defines a communication controller that can be used to send requests to the Danfoss Air Unit.
  * 
- * @author Jacob Laursen - Refactoring, bugfixes and enhancements
+ * @author Jacob Laursen - Initial contribution
  */
 @NonNullByDefault
 public interface CommunicationController {
diff --git a/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/DanfossAirUnit.java b/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/DanfossAirUnit.java
index 0e3646281ed17..addb36f253904 100644
--- a/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/DanfossAirUnit.java
+++ b/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/DanfossAirUnit.java
@@ -213,7 +213,16 @@ public DecimalType getBatteryLife() throws IOException {
     }
 
     public DecimalType getFilterLife() throws IOException {
-        return new DecimalType(BigDecimal.valueOf(asPercentByte(getByte(REGISTER_1_READ, FILTER_LIFE))));
+        BigDecimal value = BigDecimal.valueOf(asPercentByte(getByte(REGISTER_1_READ, FILTER_LIFE)));
+        return new DecimalType(value.setScale(1, RoundingMode.HALF_UP));
+    }
+
+    public DecimalType getFilterPeriod() throws IOException {
+        return new DecimalType(BigDecimal.valueOf(getByte(REGISTER_1_READ, FILTER_PERIOD)));
+    }
+
+    public DecimalType setFilterPeriod(Command cmd) throws IOException {
+        return setNumberTypeRegister(cmd, FILTER_PERIOD);
     }
 
     public DateTimeType getCurrentTime() throws IOException, UnexpectedResponseValueException {
@@ -225,6 +234,14 @@ public PercentType setManualFanStep(Command cmd) throws IOException {
         return setPercentTypeRegister(cmd, MANUAL_FAN_SPEED_STEP);
     }
 
+    private DecimalType setNumberTypeRegister(Command cmd, byte[] register) throws IOException {
+        if (cmd instanceof DecimalType) {
+            byte value = (byte) ((DecimalType) cmd).intValue();
+            set(REGISTER_1_WRITE, register, value);
+        }
+        return new DecimalType(BigDecimal.valueOf(getByte(REGISTER_1_READ, register)));
+    }
+
     private PercentType setPercentTypeRegister(Command cmd, byte[] register) throws IOException {
         if (cmd instanceof PercentType) {
             byte value = (byte) ((((PercentType) cmd).intValue() + 5) / 10);
diff --git a/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/discovery/DanfossAirUnitDiscoveryService.java b/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/discovery/DanfossAirUnitDiscoveryService.java
index df98da487953e..8b96398b197eb 100644
--- a/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/discovery/DanfossAirUnitDiscoveryService.java
+++ b/bundles/org.openhab.binding.danfossairunit/src/main/java/org/openhab/binding/danfossairunit/internal/discovery/DanfossAirUnitDiscoveryService.java
@@ -80,7 +80,6 @@ private synchronized void discover() {
         logger.debug("Try to discover all Danfoss Air CCM devices");
 
         try (DatagramSocket socket = new DatagramSocket()) {
-
             Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
             while (interfaces.hasMoreElements()) {
                 NetworkInterface networkInterface = interfaces.nextElement();
@@ -96,7 +95,6 @@ private synchronized void discover() {
                     sendBroadcastToDiscoverThing(socket, interfaceAddress.getBroadcast());
                 }
             }
-
         } catch (IOException e) {
             logger.debug("No Danfoss Air CCM device found. Diagnostic: {}", e.getMessage());
         }
diff --git a/bundles/org.openhab.binding.danfossairunit/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.danfossairunit/src/main/resources/OH-INF/thing/thing-types.xml
index fe22f1e907b94..6fa06e5e0b046 100644
--- a/bundles/org.openhab.binding.danfossairunit/src/main/resources/OH-INF/thing/thing-types.xml
+++ b/bundles/org.openhab.binding.danfossairunit/src/main/resources/OH-INF/thing/thing-types.xml
@@ -124,6 +124,7 @@
 				<label>Remaining Filter Life</label>
 				<description>Remaining life of filter until exchange is necessary</description>
 			</channel>
+			<channel id="filter_period" typeId="filterPeriod"/>
 		</channels>
 	</channel-group-type>
 
@@ -190,6 +191,12 @@
 		<category>Fan</category>
 		<state step="10" min="0" max="100" readOnly="true"/>
 	</channel-type>
+	<channel-type id="filterPeriod" advanced="true">
+		<item-type>Number</item-type>
+		<label>Filter Period</label>
+		<description>Number of months between filter replacements</description>
+		<state pattern="%d" min="3" max="12"/>
+	</channel-type>
 	<channel-type id="percentage">
 		<item-type>Number</item-type>
 		<label>Percentage</label>
diff --git a/bundles/org.openhab.binding.danfossairunit/src/test/java/org/openhab/binding/danfossairunit/internal/DanfossAirUnitTest.java b/bundles/org.openhab.binding.danfossairunit/src/test/java/org/openhab/binding/danfossairunit/internal/DanfossAirUnitTest.java
index f515f9d323a3f..8bb966b19b92c 100644
--- a/bundles/org.openhab.binding.danfossairunit/src/test/java/org/openhab/binding/danfossairunit/internal/DanfossAirUnitTest.java
+++ b/bundles/org.openhab.binding.danfossairunit/src/test/java/org/openhab/binding/danfossairunit/internal/DanfossAirUnitTest.java
@@ -23,6 +23,7 @@
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 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.PercentType;
 import org.openhab.core.library.types.QuantityType;
@@ -31,7 +32,7 @@
 /**
  * This class provides test cases for {@link DanfossAirUnit}
  * 
- * @author Jacob Laursen - Refactoring, bugfixes and enhancements
+ * @author Jacob Laursen - Initial contribution
  */
 public class DanfossAirUnitTest extends JavaTest {
 
@@ -153,4 +154,12 @@ public void getManualFanStepWhenOutOfRangeThrows() throws IOException {
         var airUnit = new DanfossAirUnit(communicationController);
         assertThrows(UnexpectedResponseValueException.class, () -> airUnit.getManualFanStep());
     }
+
+    @Test
+    public void getFilterLifeWhenNearestNeighborIsBelowRoundsDown() throws IOException {
+        byte[] response = new byte[] { (byte) 0xf0 };
+        when(this.communicationController.sendRobustRequest(REGISTER_1_READ, FILTER_LIFE)).thenReturn(response);
+        var airUnit = new DanfossAirUnit(communicationController);
+        assertEquals(new DecimalType("94.1"), airUnit.getFilterLife());
+    }
 }