Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 6 additions & 6 deletions bundles/org.openhab.binding.ondilo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,19 +99,19 @@ Example using default interval:

```Java
Bridge ondilo:account:ondiloAccount [ url="http://localhost:8080", refreshInterval=900 ] {
Thing ondilo 12345 [ id=12345 ] { // 12345 is an example of the id received via discovery
}
Thing ondilo 12345 [ id=12345 ] // 12345 is an example of the id received via discovery
}
```

### Item Configuration

```java
Number:Temperature Ondilo_Temperature "Pool Temperature [%.1f %unit%]" { channel="ondilo:ondilo:ondiloAccount:12345:measure#temperature" }
Number Ondilo_pH "Pool pH [%d]" { channel="ondilo:ondilo:ondiloAccount:12345:measure#ph" }
Number:ElectricPotential Ondilo_ORP "Pool ORP [%.1f %unit%]" { channel="ondilo:ondilo:ondiloAccount:12345:measure#orp" }
Number Ondilo_pH "Pool pH [%.2f]" { channel="ondilo:ondilo:ondiloAccount:12345:measure#ph" }
Number:ElectricPotential Ondilo_ORP "Pool ORP [%.0f %unit%]" { channel="ondilo:ondilo:ondiloAccount:12345:measure#orp" }
Number:Density Ondilo_Salt "Pool Salt [%.0f %unit%]" { channel="ondilo:ondilo:ondiloAccount:12345:measure#salt" }
Number:Dimensionless Ondilo_Battery "Pool Battery [%d %]" { channel="ondilo:ondilo:ondiloAccount:12345:measure#battery" }
Number:Dimensionless Ondilo_RSSI "Pool RSSI [%.0f]" { channel="ondilo:ondilo:ondiloAccount:12345:measure#rssi" }
Number:Dimensionless Ondilo_Battery "Pool Battery [%d %%]" { channel="ondilo:ondilo:ondiloAccount:12345:measure#battery" }
Number:Dimensionless Ondilo_RSSI "Pool RSSI [%d]" { channel="ondilo:ondilo:ondiloAccount:12345:measure#rssi" }

String Ondilo_RecTitle "Recommendation Title [%s]" { channel="ondilo:ondilo:ondiloAccount:12345:recommendation#title" }
String Ondilo_RecMessage "Recommendation Message [%s]" { channel="ondilo:ondilo:ondiloAccount:12345:recommendation#message" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,15 +111,15 @@ public synchronized void pollOndiloICOs() {
if (pools != null && !(pools.length == 0)) {
logger.trace("Polled {} Ondilo ICOs", pools.length);
// Poll last measures and recommendations for each pool
Instant lastValueTime = null;
Instant earliestValueTime = null;
for (Pool pool : pools) {
Instant valueTime = pollOndiloICO(pool.id);
if (lastValueTime == null || (valueTime != null && valueTime.isBefore(lastValueTime))) {
lastValueTime = valueTime;
if (valueTime != null && (earliestValueTime == null || valueTime.isBefore(earliestValueTime))) {
earliestValueTime = valueTime;
}
}
if (lastValueTime != null) {
adaptPollingToValueTime(lastValueTime, refreshInterval);
if (earliestValueTime != null) {
adaptPollingToValueTime(earliestValueTime, refreshInterval);
}
} else {
logger.warn("No Ondilo ICO found or failed to parse JSON response");
Expand All @@ -134,10 +134,10 @@ public synchronized void pollOndiloICOs() {
}
}

private void adaptPollingToValueTime(Instant lastValueTime, int refreshInterval) {
private void adaptPollingToValueTime(Instant earliestValueTime, int refreshInterval) {
// Adjusting the polling reduces the delay when new measures get available, without polling too frequently and
// hitting API rate limits.
Instant nextValueTime = lastValueTime.plus(TARGET_REFRESH_INTERVAL);
Instant nextValueTime = earliestValueTime.plus(TARGET_REFRESH_INTERVAL);
Instant now = Instant.now();
Instant scheduledTime = now.plusSeconds(refreshInterval);
if (nextValueTime.isBefore(scheduledTime)) {
Expand All @@ -157,7 +157,7 @@ private void adaptPollingToValueTime(Instant lastValueTime, int refreshInterval)
public @Nullable Instant pollOndiloICO(int id) {
OndiloHandler ondiloHandler = getOndiloHandlerForPool(id);
OndiloApiClient apiClient = this.apiClient;
Instant lastValueTime = null;
Instant earliestValueTime = null;
if (ondiloHandler != null && apiClient != null) {
LastMeasure[] lastMeasures = apiClient.request("GET", "/pools/" + id
+ "/lastmeasures?types[]=temperature&types[]=ph&types[]=orp&types[]=salt&types[]=tds&types[]=battery&types[]=rssi",
Expand All @@ -167,13 +167,7 @@ private void adaptPollingToValueTime(Instant lastValueTime, int refreshInterval)
logger.warn("No lastMeasures available for Ondilo ICO with ID: {}", id);
ondiloHandler.clearLastMeasuresChannels();
} else {
for (LastMeasure lastMeasure : lastMeasures) {
logger.trace("LastMeasure: type={}, value={}", lastMeasure.dataType, lastMeasure.value);
Instant valueTime = ondiloHandler.updateLastMeasuresChannels(lastMeasure);
if (lastValueTime == null || valueTime.isBefore(lastValueTime)) {
lastValueTime = valueTime;
}
}
earliestValueTime = ondiloHandler.updateLastMeasuresChannels(lastMeasures);
}

Recommendation[] recommendations = apiClient.request("GET", "/pools/" + id + "/recommendations",
Expand Down Expand Up @@ -216,7 +210,7 @@ private void adaptPollingToValueTime(Instant lastValueTime, int refreshInterval)
} else {
logger.debug("No OndiloHandler found for Ondilo ICO with ID: {}", id);
}
return lastValueTime;
return earliestValueTime;
}

public void validateRecommendation(int poolId, int recommendationId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,13 @@ public class OndiloHandler extends BaseThingHandler {
private final LocaleProvider localeProvider;
private final int configPoolId;

private int recommendationId; // Used to track the last recommendation ID processed
private AtomicReference<String> ondiloId = new AtomicReference<>(NO_ID);

private @Nullable ScheduledFuture<?> bridgeRecoveryJob;

private @Nullable LastMeasure[] lastMeasures = new LastMeasure[0];
private @Nullable Recommendation lastRecommendation = null;

// Store last value and valueTime for trend calculation
private OndiloMeasureState lastTemperatureState = new OndiloMeasureState(Double.NaN, null);
private OndiloMeasureState lastPhState = new OndiloMeasureState(Double.NaN, null);
Expand All @@ -88,17 +90,35 @@ public OndiloHandler(Thing thing, LocaleProvider localeProvider) {
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (RefreshType.REFRESH == command) {
// not implemented as it would causes >10 channel updates in a row during setup (exceeds given API quota)
// If you want to update the values, use the poll channel instead
String groupId = channelUID.getGroupId();
if (groupId == null) {
logger.warn("Received refresh command for unknown channel: {}", channelUID.getId());
} else if (GROUP_MEASURES.startsWith(groupId)) {
if (lastMeasures.length == 0) {
clearLastMeasuresChannels();
} else {
updateLastMeasuresChannels(lastMeasures);
}
} else if (GROUP_RECOMMENDATIONS.startsWith(groupId)) {
Recommendation lastRecommendation = this.lastRecommendation;
if (lastRecommendation == null) {
clearRecommendationChannels();
} else {
updateRecommendationChannels(lastRecommendation);
}
} else {
logger.warn("Received refresh command for unknown channel: {}", channelUID.getId());
}
return;
} else if (CHANNEL_RECOMMENDATION_STATUS.equals(channelUID.getId())) {
if (command instanceof StringType cmd) {
try {
Recommendation.Status status = Recommendation.Status.valueOf(cmd.toString());
if (status == Recommendation.Status.ok) {
OndiloBridge ondiloBridge = getOndiloBridge();
if (ondiloBridge != null && this.recommendationId != 0) {
ondiloBridge.validateRecommendation(configPoolId, recommendationId);
Recommendation lastRecommendation = this.lastRecommendation;
if (ondiloBridge != null && lastRecommendation != null && lastRecommendation.id != 0) {
ondiloBridge.validateRecommendation(configPoolId, lastRecommendation.id);
} else {
logger.warn(
"Cannot validate recommendation, as the bridge is not initialized or no recommendation ID is set");
Expand All @@ -120,8 +140,9 @@ public void handleCommand(ChannelUID channelUID, Command command) {
public void initialize() {
OndiloBridge ondiloBridge = getOndiloBridge();
if (ondiloBridge != null) {
// Initialize to 0, as no recommendation has been processed yet
recommendationId = 0;
// Initialize to empty array, as no measure / recommendation has been processed yet
this.lastMeasures = new LastMeasure[0];
this.lastRecommendation = null;

if (configPoolId == 0) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, I18N_ID_INVALID);
Expand Down Expand Up @@ -163,7 +184,10 @@ public void dispose() {
if (!ondiloId.get().equals(NO_ID)) {
ondiloId.set(NO_ID);
}
recommendationId = 0; // Reset last processed recommendation ID

// Initialize to empty array, as no measure / recommendation has been processed yet
this.lastMeasures = new LastMeasure[0];
this.lastRecommendation = null;
}

public void clearLastMeasuresChannels() {
Expand All @@ -175,6 +199,7 @@ public void clearLastMeasuresChannels() {
updateState(CHANNEL_TDS, UnDefType.UNDEF);
updateState(CHANNEL_BATTERY, UnDefType.UNDEF);
updateState(CHANNEL_RSSI, UnDefType.UNDEF);
this.lastMeasures = new LastMeasure[0];
}

public void clearRecommendationChannels() {
Expand All @@ -186,7 +211,7 @@ public void clearRecommendationChannels() {
updateState(CHANNEL_RECOMMENDATION_UPDATED_AT, UnDefType.NULL);
updateState(CHANNEL_RECOMMENDATION_STATUS, UnDefType.NULL);
updateState(CHANNEL_RECOMMENDATION_DEADLINE, UnDefType.NULL);
this.recommendationId = 0; // Reset last processed recommendation ID
this.lastRecommendation = null;
}

private void updateTrendChannel(String channel, String trendChannel, double value, Instant valueTime,
Expand Down Expand Up @@ -219,41 +244,53 @@ private void updateTrendChannel(String channel, String trendChannel, double valu
lastMeasureState.time = valueTime;
}

public Instant updateLastMeasuresChannels(LastMeasure measure) {
Instant valueTime = parseUtcTimeToInstant(measure.valueTime);
switch (measure.dataType) {
case "temperature":
updateTrendChannel(CHANNEL_TEMPERATURE, CHANNEL_TEMPERATURE_TREND, measure.value, valueTime,
lastTemperatureState, SIUnits.CELSIUS);
break;
case "ph":
updateTrendChannel(CHANNEL_PH, CHANNEL_PH_TREND, measure.value, valueTime, lastPhState,
DecimalType.class);
break;
case "orp":
updateTrendChannel(CHANNEL_ORP, CHANNEL_ORP_TREND, measure.value / 1000.0, valueTime, lastOrpState,
Units.VOLT); // Convert mV to V
break;
case "salt":
updateTrendChannel(CHANNEL_SALT, CHANNEL_SALT_TREND, measure.value * 0.001, valueTime, lastSaltState,
Units.KILOGRAM_PER_CUBICMETRE); // Convert mg/l to kg/m³
break;
case "tds":
updateTrendChannel(CHANNEL_TDS, CHANNEL_TDS_TREND, measure.value, valueTime, lastTdsState,
Units.PARTS_PER_MILLION);
break;
case "battery":
updateState(CHANNEL_BATTERY, new QuantityType<>(measure.value, Units.PERCENT));
break;
case "rssi":
updateState(CHANNEL_RSSI, new DecimalType(measure.value));
break;
default:
logger.warn("Unknown data type: {}", measure.dataType);
public @Nullable Instant updateLastMeasuresChannels(LastMeasure[] measures) {
Instant earliestValueTime = null;
for (LastMeasure measure : measures) {
logger.trace("LastMeasure: type={}, value={}", measure.dataType, measure.value);
Instant valueTime = parseUtcTimeToInstant(measure.valueTime);
if (earliestValueTime == null || valueTime.isBefore(earliestValueTime)) {
earliestValueTime = valueTime;
}
switch (measure.dataType) {
case "temperature":
updateTrendChannel(CHANNEL_TEMPERATURE, CHANNEL_TEMPERATURE_TREND, measure.value, valueTime,
lastTemperatureState, SIUnits.CELSIUS);
break;
case "ph":
updateTrendChannel(CHANNEL_PH, CHANNEL_PH_TREND, measure.value, valueTime, lastPhState,
DecimalType.class);
break;
case "orp":
updateTrendChannel(CHANNEL_ORP, CHANNEL_ORP_TREND, measure.value / 1000.0, valueTime, lastOrpState,
Units.VOLT); // Convert mV to V
break;
case "salt":
updateTrendChannel(CHANNEL_SALT, CHANNEL_SALT_TREND, measure.value * 0.001, valueTime,
lastSaltState, Units.KILOGRAM_PER_CUBICMETRE); // Convert mg/l to kg/m³
break;
case "tds":
updateTrendChannel(CHANNEL_TDS, CHANNEL_TDS_TREND, measure.value, valueTime, lastTdsState,
Units.PARTS_PER_MILLION);
break;
case "battery":
updateState(CHANNEL_BATTERY, new QuantityType<>(measure.value, Units.PERCENT));
break;
case "rssi":
updateState(CHANNEL_RSSI, new DecimalType(measure.value));
break;
default:
logger.warn("Unknown data type: {}", measure.dataType);
}
}
// Update value time channel (expect that it is the same for all measures)
updateState(CHANNEL_VALUE_TIME, new DateTimeType(valueTime));
return valueTime;

if (earliestValueTime != null) {
// Update value time channel (expect that it is the same for all measures)
updateState(CHANNEL_VALUE_TIME, new DateTimeType(earliestValueTime));
}

this.lastMeasures = measures;
return earliestValueTime;
}

public void updateRecommendationChannels(Recommendation recommendation) {
Expand All @@ -264,7 +301,7 @@ public void updateRecommendationChannels(Recommendation recommendation) {
updateState(CHANNEL_RECOMMENDATION_UPDATED_AT, new DateTimeType(recommendation.updatedAt));
updateState(CHANNEL_RECOMMENDATION_STATUS, new StringType(recommendation.status.name()));
updateState(CHANNEL_RECOMMENDATION_DEADLINE, new DateTimeType(recommendation.deadline));
this.recommendationId = recommendation.id; // Update last processed recommendation ID
this.lastRecommendation = recommendation;
}

public void updatePool(Pool pool) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@
<tags>
<tag>Measurement</tag>
</tags>
<state readOnly="true" pattern="%.1f %unit%"/>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="orp-trend" advanced="true">
<item-type unitHint="mV">Number:ElectricPotential</item-type>
Expand All @@ -198,7 +198,7 @@
<tags>
<tag>Measurement</tag>
</tags>
<state readOnly="true" pattern="Δ %+.1f %unit%"/>
<state readOnly="true" pattern="Δ %+.0f %unit%"/>
</channel-type>
<channel-type id="salt">
<item-type unitHint="mg/l">Number:Density</item-type>
Expand Down