Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
61d257e
[rachio] Initial contribution
Jun 27, 2026
e22e2d7
[rachio] Use zero for idle active zone number
Jul 19, 2026
0d7a4b8
[rachio] Add binding to add-ons BOM
Jul 19, 2026
032382c
[rachio] Reduce PR diff for Copilot review
Jul 22, 2026
966217f
Potential fix for pull request finding
kovacsi2899 Jul 22, 2026
59ef577
[rachio] Address Copilot configuration findings
Jul 22, 2026
fb7931a
[rachio] Address additional Copilot review findings
Jul 22, 2026
cb107f4
[rachio] Address Copilot metadata findings
Jul 22, 2026
5c73fb3
Apply suggestion from @wborn
kovacsi2899 Jul 22, 2026
62d57db
[rachio] Address webhook and timeout findings
Jul 22, 2026
e3f511d
[rachio] Address unit hint and listener style findings
Jul 22, 2026
da053ec
[rachio] Address forwarded IP and priority style findings
Jul 22, 2026
5a974af
[rachio] Address property and zone channel findings
Jul 22, 2026
8b21ca1
[rachio] Address cloud webhook registry findings
Jul 22, 2026
ff71773
[rachio] Synchronize rate limit manager state
Jul 22, 2026
e9d5a45
[rachio] Document rate limit manager synchronization
Jul 22, 2026
3373b91
[rachio] Address HTTP stream and README findings
Jul 22, 2026
fdd708b
[rachio] Address schedule review findings
Jul 22, 2026
ad2ea8c
Update Rachio metadata descriptions
Jul 23, 2026
0c5b010
[rachio] Fix zone number state pattern
Jul 23, 2026
4fdda0e
[rachio] Address webhook review findings
Jul 23, 2026
b237aea
[rachio] Harden listener and date parsing
Jul 23, 2026
0305340
[rachio] Align runtime default and DTO fields
Jul 23, 2026
6ce155e
[rachio] Preserve API exception causes
Jul 23, 2026
44bc8f7
[rachio] Use API exception cause directly
Jul 23, 2026
aed4919
[rachio] Address Copilot cleanup findings
Jul 23, 2026
e3f08ca
[rachio] Address webhook review cleanup
Jul 23, 2026
0f70c4f
[rachio] Validate rate limit manager inputs
Jul 23, 2026
16a3395
[rachio] Address final Copilot findings
Jul 23, 2026
d387e8c
[rachio] Address final review cleanup
Jul 23, 2026
231eefe
[rachio] Address maintainer cleanup comments
Jul 25, 2026
ca9c359
[rachio] Use collection interfaces in DTOs
Jul 25, 2026
0047e1f
[rachio] Address maintainer review feedback
Jul 26, 2026
9882c89
[rachio] Harden webhook and model synchronization
Aug 2, 2026
e723558
[rachio] Close webhook synchronization races
Aug 2, 2026
13ec444
[rachio] Ignore stale cloud watering timestamps
Aug 2, 2026
2a94bd6
[rachio] Stabilize discovery snapshots
Aug 8, 2026
c28e962
[rachio] Restore binding test coverage
Aug 8, 2026
04593a4
[rachio] Stabilize cloud webhook tests
Aug 8, 2026
80cae46
[rachio] Address README review feedback
Aug 8, 2026
7424fd2
[rachio] Harden webhook reconciliation and HTTP handling
Aug 8, 2026
276bf38
[rachio] Polish webhook callback handling
Aug 9, 2026
77d1795
[rachio] Reduce routine logging to debug
kovacsi2899 Aug 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@
/bundles/org.openhab.binding.pushsafer/ @appzer @cweitkamp
/bundles/org.openhab.binding.qbus/ @QbusKoen
/bundles/org.openhab.binding.qolsysiq/ @digitaldan
/bundles/org.openhab.binding.rachio/ @kovacsi2899
/bundles/org.openhab.binding.radiobrowser/ @skinah
/bundles/org.openhab.binding.radiothermostat/ @mlobstein
/bundles/org.openhab.binding.regoheatpump/ @crnjan
Expand Down
5 changes: 5 additions & 0 deletions bom/openhab-addons/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1666,6 +1666,11 @@
<artifactId>org.openhab.binding.qolsysiq</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.rachio</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.radiobrowser</artifactId>
Expand Down
13 changes: 13 additions & 0 deletions bundles/org.openhab.binding.rachio/NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.

* Project home: https://www.openhab.org

== Declared Project Licenses

This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.

== Source Code

https://github.com/openhab/openhab-addons
275 changes: 275 additions & 0 deletions bundles/org.openhab.binding.rachio/README.md

Large diffs are not rendered by default.

26 changes: 26 additions & 0 deletions bundles/org.openhab.binding.rachio/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>5.3.0-SNAPSHOT</version>
</parent>

<artifactId>org.openhab.binding.rachio</artifactId>

<name>openHAB Add-ons :: Bundles :: Rachio Binding</name>

<dependencies>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.rest</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.rachio-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>

<feature name="openhab-binding-rachio" description="Rachio Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.rachio/${project.version}</bundle>
</feature>
</features>

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
/*
* Copyright (c) 2010-2026 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.rachio.internal;

import static org.openhab.binding.rachio.internal.RachioBindingConstants.*;

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* The {@link RachioConfiguration} contains the Rachio Cloud Connector Thing configuration and default values. The field
* names represent the configuration names, do not rename them if you don't intend to break the configuration interface.
*
* @author Markus Michels - Initial contribution
*/
@NonNullByDefault
public class RachioConfiguration {
private static final String REDACTED = "[redacted]";
private static final List<String> CONFIGURATION_PARAMETERS = List.of(PARAM_APIKEY, PARAM_POLLING_INTERVAL,
PARAM_DEFAULT_RUNTIME, PARAM_CALLBACK_URL, PARAM_CALLBACK_USERNAME, PARAM_CALLBACK_PASSWORD,
PARAM_CLEAR_CALLBACK, PARAM_USE_CLOUD_WEBHOOK, PARAM_AUTO_CONFIGURE_WEBHOOKS, PARAM_PUBLIC_WEBHOOK_URL,
PARAM_AUTO_CONFIGURE_HOSE_TIMER_WEBHOOKS, PARAM_EVENT_HISTORY_LOOKBACK_HOURS, PARAM_FORECAST_UNITS,
PARAM_HOSE_SUMMARY_LOOKBACK_DAYS, PARAM_HOSE_SUMMARY_LOOKAHEAD_DAYS);

private final Logger logger = LoggerFactory.getLogger(RachioConfiguration.class);
private final Set<String> configuredParameters = new HashSet<>();

public String apikey = "";
public int pollingInterval = DEFAULT_POLLING_INTERVAL_SEC;
public int defaultRuntime = DEFAULT_ZONE_RUNTIME_SEC;
public String callbackUrl = "";
public String callbackUsername = "";
public String callbackPassword = "";
public boolean clearAllCallbacks = false;
public boolean useCloudWebhook = false;
public boolean autoConfigureWebhooks = false;
public boolean autoConfigureHoseTimerWebhooks = false;
public String publicWebhookUrl = "";
public int eventHistoryLookbackHours = DEFAULT_EVENT_HISTORY_LOOKBACK_HOURS;
public String forecastUnits = DEFAULT_FORECAST_UNITS;
public int hoseSummaryLookbackDays = DEFAULT_HOSE_SUMMARY_LOOKBACK_DAYS;
public int hoseSummaryLookaheadDays = DEFAULT_HOSE_SUMMARY_LOOKAHEAD_DAYS;

public RachioConfiguration() {
}

public enum ConfigurationSource {
CLOUD_THING("Cloud Thing"),
DEFAULT("built-in default");

private final String label;

ConfigurationSource(String label) {
this.label = label;
}

public String label() {
return label;
}
}

public static class ResolvedConfiguration {
private final RachioConfiguration configuration;
private final Map<String, ConfigurationSource> sources;

private ResolvedConfiguration(RachioConfiguration configuration, Map<String, ConfigurationSource> sources) {
this.configuration = configuration;
this.sources = Map.copyOf(sources);
}

public RachioConfiguration configuration() {
return configuration;
}

public ConfigurationSource source(String parameterName) {
String canonicalParameterName = canonicalParameterName(parameterName);
if (canonicalParameterName == null) {
return ConfigurationSource.DEFAULT;
}
return sources.getOrDefault(canonicalParameterName, ConfigurationSource.DEFAULT);
}
}

public static ResolvedConfiguration resolveEffectiveConfig(
@Nullable Map<String, @Nullable Object> cloudThingConfig) {
RachioConfiguration effectiveConfig = new RachioConfiguration();
Map<String, ConfigurationSource> sources = new HashMap<>();
for (String parameterName : CONFIGURATION_PARAMETERS) {
sources.put(parameterName, ConfigurationSource.DEFAULT);
}

effectiveConfig.updateConfig(cloudThingConfig);
if (cloudThingConfig != null) {
for (Map.Entry<String, @Nullable Object> entry : cloudThingConfig.entrySet()) {
if (entry.getValue() == null) {
continue;
}
String parameterName = canonicalParameterName(entry.getKey());
if (parameterName != null) {
sources.put(parameterName, ConfigurationSource.CLOUD_THING);
}
}
}

return new ResolvedConfiguration(effectiveConfig, sources);
}

public void updateConfig(@Nullable Map<String, @Nullable Object> config) {
if (config == null) {
return;
}
for (Map.Entry<String, @Nullable Object> ce : config.entrySet()) {
String key = ce.getKey();
if ("component.name".equalsIgnoreCase(key) || "component.id".equalsIgnoreCase(key)) {
continue;
}
if (ce.getValue() == null) {
// no value set
continue;
}
Object configValue = ce.getValue();
String value = configValue != null ? configValue.toString() : "";

if ("service.pid".equalsIgnoreCase(key)) {
logger.debug("Rachio: Cloud Connector configuration:");
}
logger.debug(" {}={}", key, sanitizeValueForLogging(key, value));

String parameterName = canonicalParameterName(key);
if (parameterName == null) {
continue;
}

configuredParameters.add(parameterName);
if (parameterName.equals(PARAM_APIKEY)) {
apikey = value;
} else if (parameterName.equals(PARAM_POLLING_INTERVAL)) {
this.pollingInterval = parsePositiveSeconds(value, DEFAULT_POLLING_INTERVAL_SEC,
PARAM_POLLING_INTERVAL);
} else if (parameterName.equals(PARAM_DEFAULT_RUNTIME)) {
this.defaultRuntime = parsePositiveSeconds(value, DEFAULT_ZONE_RUNTIME_SEC, PARAM_DEFAULT_RUNTIME);
} else if (parameterName.equals(PARAM_CALLBACK_URL)) {
this.callbackUrl = value;
} else if (parameterName.equals(PARAM_CALLBACK_USERNAME)) {
this.callbackUsername = value;
} else if (parameterName.equals(PARAM_CALLBACK_PASSWORD)) {
this.callbackPassword = value;
} else if (parameterName.equals(PARAM_CLEAR_CALLBACK)) {
this.clearAllCallbacks = Boolean.parseBoolean(value);
} else if (parameterName.equals(PARAM_USE_CLOUD_WEBHOOK)) {
this.useCloudWebhook = Boolean.parseBoolean(value);
} else if (parameterName.equals(PARAM_AUTO_CONFIGURE_WEBHOOKS)) {
this.autoConfigureWebhooks = Boolean.parseBoolean(value);
} else if (parameterName.equals(PARAM_AUTO_CONFIGURE_HOSE_TIMER_WEBHOOKS)) {
this.autoConfigureHoseTimerWebhooks = Boolean.parseBoolean(value);
} else if (parameterName.equals(PARAM_PUBLIC_WEBHOOK_URL)) {
this.publicWebhookUrl = value;
} else if (parameterName.equals(PARAM_EVENT_HISTORY_LOOKBACK_HOURS)) {
this.eventHistoryLookbackHours = parseEventHistoryLookbackHours(value);
} else if (parameterName.equals(PARAM_FORECAST_UNITS)) {
this.forecastUnits = parseForecastUnits(value);
} else if (parameterName.equals(PARAM_HOSE_SUMMARY_LOOKBACK_DAYS)) {
this.hoseSummaryLookbackDays = parseSummaryWindowDays(value, DEFAULT_HOSE_SUMMARY_LOOKBACK_DAYS,
PARAM_HOSE_SUMMARY_LOOKBACK_DAYS);
} else if (parameterName.equals(PARAM_HOSE_SUMMARY_LOOKAHEAD_DAYS)) {
this.hoseSummaryLookaheadDays = parseSummaryWindowDays(value, DEFAULT_HOSE_SUMMARY_LOOKAHEAD_DAYS,
PARAM_HOSE_SUMMARY_LOOKAHEAD_DAYS);
}
}
}

public boolean hasConfiguredValue(String parameterName) {
String canonicalParameterName = canonicalParameterName(parameterName);
return canonicalParameterName != null && configuredParameters.contains(canonicalParameterName);
}

private static @Nullable String canonicalParameterName(String key) {
for (String parameterName : CONFIGURATION_PARAMETERS) {
if (parameterName.equalsIgnoreCase(key)) {
return parameterName;
}
}
return null;
}

private int parsePositiveSeconds(String value, int defaultValue, String parameterName) {
try {
int seconds = Integer.parseInt(value.trim());
if (seconds <= 0) {
logger.warn("Invalid Rachio {} '{}'; using default {}.", parameterName, value, defaultValue);
return defaultValue;
}
return seconds;
} catch (NumberFormatException e) {
logger.warn("Invalid Rachio {} '{}'; using default {}.", parameterName, value, defaultValue);
return defaultValue;
}
}

private int parseEventHistoryLookbackHours(String value) {
try {
int lookbackHours = Integer.parseInt(value.trim());
if (lookbackHours < 0) {
logger.warn("Invalid Rachio eventHistoryLookbackHours '{}'; using 0 to disable event history polling.",
value);
return 0;
}
if (lookbackHours > MAX_EVENT_HISTORY_LOOKBACK_HOURS) {
logger.warn("Rachio eventHistoryLookbackHours '{}' is too large; using maximum {}.", value,
MAX_EVENT_HISTORY_LOOKBACK_HOURS);
return MAX_EVENT_HISTORY_LOOKBACK_HOURS;
}
return lookbackHours;
} catch (NumberFormatException e) {
logger.warn("Invalid Rachio eventHistoryLookbackHours '{}'; using default {}.", value,
DEFAULT_EVENT_HISTORY_LOOKBACK_HOURS);
return DEFAULT_EVENT_HISTORY_LOOKBACK_HOURS;
}
}

private String parseForecastUnits(String value) {
String normalizedValue = value.trim().toUpperCase(Locale.ROOT);
if ("METRIC".equals(normalizedValue) || "US".equals(normalizedValue)) {
return normalizedValue;
}
logger.warn("Invalid Rachio forecastUnits '{}'; using default {}.", value, DEFAULT_FORECAST_UNITS);
return DEFAULT_FORECAST_UNITS;
}

private int parseSummaryWindowDays(String value, int defaultValue, String parameterName) {
try {
int days = Integer.parseInt(value.trim());
if (days < 0) {
logger.warn("Invalid Rachio {} '{}'; using 0.", parameterName, value);
return 0;
}
if (days > MAX_HOSE_SUMMARY_WINDOW_DAYS) {
logger.warn("Rachio {} '{}' is too large; using maximum {}.", parameterName, value,
MAX_HOSE_SUMMARY_WINDOW_DAYS);
return MAX_HOSE_SUMMARY_WINDOW_DAYS;
}
return days;
} catch (NumberFormatException e) {
logger.warn("Invalid Rachio {} '{}'; using default {}.", parameterName, value, defaultValue);
return defaultValue;
}
}

private String sanitizeValueForLogging(String key, String value) {
if (key.equalsIgnoreCase(PARAM_APIKEY)) {
return REDACTED;
}
if (key.equalsIgnoreCase(PARAM_CALLBACK_USERNAME)) {
return REDACTED;
}
if (key.equalsIgnoreCase(PARAM_CALLBACK_PASSWORD)) {
return REDACTED;
}
if (key.equalsIgnoreCase(PARAM_CALLBACK_URL)) {
return sanitizeCallbackUrlForLogging(value);
}
if (key.equalsIgnoreCase(PARAM_PUBLIC_WEBHOOK_URL)) {
return sanitizeCallbackUrlForLogging(value);
}
return value;
}

private String sanitizeCallbackUrlForLogging(String value) {
if (value.isBlank()) {
return value;
}
return REDACTED;
}
}
Loading