Skip to content

Commit

Permalink
[Openuv] Providing an iconserver (openhab#15191)
Browse files Browse the repository at this point in the history
* Adding an icon server to OpenUV binding

---------

Signed-off-by: clinique <gael@lhopital.org>
Signed-off-by: Jørgen Austvik <jaustvik@acm.org>
  • Loading branch information
clinique authored and austvik committed Mar 27, 2024
1 parent 4c6304f commit 2ce9ede
Show file tree
Hide file tree
Showing 24 changed files with 945 additions and 81 deletions.
10 changes: 10 additions & 0 deletions bundles/org.openhab.binding.openuv/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ This is quite useful with a free OpenUV account (50 req/day included): in this c

Thing can be extended with as many SafeExposure channels as needed for each skin type.

## Provided icon set

This binding has its own IconProvider and makes available the following list of icons

| Icon Name | Dynamic | Illustration |
|--------------------|---------|--------------|
| oh:openuv:ozone | No | ![](src/main/resources/icon/ozone.svg) |
| oh:openuv:uv-alarm | Yes | ![](src/main/resources/icon/uv-alarm.svg) |
| oh:openuv:uv-index | Yes | ![](src/main/resources/icon/uv-index.svg) |

## Examples

demo.things:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* Copyright (c) 2010-2023 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.openuv.internal;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;

/**
* The {@link AlertLevel} enum defines alert level in regard of the UV Index
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public enum AlertLevel {
GREEN(DecimalType.ZERO, "3a8b2f"),
YELLOW(new DecimalType(1), "f9a825"),
ORANGE(new DecimalType(2), "ef6c00"),
RED(new DecimalType(3), "b71c1c"),
PURPLE(new DecimalType(4), "6a1b9a"),
UNKNOWN(UnDefType.NULL, "b3b3b3");

public final State state;
public final String color;

AlertLevel(State state, String color) {
this.state = state;
this.color = color;
}

public static AlertLevel fromUVIndex(double uv) {
if (uv >= 11) {
return PURPLE;
} else if (uv >= 8) {
return RED;
} else if (uv >= 6) {
return ORANGE;
} else if (uv >= 3) {
return YELLOW;
} else if (uv > 0) {
return GREEN;
}
return UNKNOWN;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ public OpenUVException(String message) {
super(message);
}

public OpenUVException(String fitzPatrickIndex, Throwable e) {
super("Unexpected Fitzpatrick index value '%s'".formatted(fitzPatrickIndex), e);
}

private boolean checkMatches(String message) {
String currentMessage = getMessage();
return currentMessage != null && currentMessage.startsWith(message);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* Copyright (c) 2010-2023 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.openuv.internal;

import static org.openhab.binding.openuv.internal.OpenUVBindingConstants.BINDING_ID;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.Set;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.ui.icon.IconProvider;
import org.openhab.core.ui.icon.IconSet;
import org.openhab.core.ui.icon.IconSet.Format;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* The {@link OpenUVIconProvider} is the class providing binding related icons.
*
* @author Gaël L'hopital - Initial contribution
*/
@Component(service = { IconProvider.class })
@NonNullByDefault
public class OpenUVIconProvider implements IconProvider {
private static final String UV_ALARM = "uv-alarm";
private static final String UV_INDEX = "uv-index";
private static final String DEFAULT_LABEL = "OpenUV Icons";
private static final String DEFAULT_DESCRIPTION = "Icons illustrating UV conditions provided by OpenUV";
private static final Set<String> ICONS = Set.of("ozone", UV_INDEX, UV_ALARM);

private final Logger logger = LoggerFactory.getLogger(OpenUVIconProvider.class);
private final BundleContext context;
private final TranslationProvider i18nProvider;

@Activate
public OpenUVIconProvider(final BundleContext context, final @Reference TranslationProvider i18nProvider) {
this.context = context;
this.i18nProvider = i18nProvider;
}

@Override
public Set<IconSet> getIconSets() {
return getIconSets(null);
}

@Override
public Set<IconSet> getIconSets(@Nullable Locale locale) {
String label = getText("label", DEFAULT_LABEL, locale);
String description = getText("decription", DEFAULT_DESCRIPTION, locale);

return Set.of(new IconSet(BINDING_ID, label, description, Set.of(Format.SVG)));
}

private String getText(String entry, String defaultValue, @Nullable Locale locale) {
String text = defaultValue;
if (locale != null) {
text = i18nProvider.getText(context.getBundle(), "iconset." + entry, defaultValue, locale);
text = text == null ? defaultValue : text;
}
return text;
}

@Override
public @Nullable Integer hasIcon(String category, String iconSetId, Format format) {
return ICONS.contains(category) && iconSetId.equals(BINDING_ID) && format == Format.SVG ? 0 : null;
}

@Override
public @Nullable InputStream getIcon(String category, String iconSetId, @Nullable String state, Format format) {
String iconName = category;
if (UV_INDEX.equals(category) && state != null) {
try {
Double numeric = Double.valueOf(state);
iconName = "%s-%d".formatted(category, numeric.intValue());
} catch (NumberFormatException e) {
logger.debug("Unable to parse {} to a numeric value", state);
}
}
String icon = getResource(iconName);
if (UV_ALARM.equals(category) && state != null) {
try {
Integer ordinal = Integer.valueOf(state);
AlertLevel alertLevel = ordinal < AlertLevel.values().length ? AlertLevel.values()[ordinal]
: AlertLevel.UNKNOWN;
icon = icon.replaceAll(AlertLevel.UNKNOWN.color, alertLevel.color);
} catch (NumberFormatException e) {
logger.debug("Unable to parse {} to a numeric value", state);
}
}
return icon.isEmpty() ? null : new ByteArrayInputStream(icon.getBytes());
}

private String getResource(String iconName) {
String result = "";

URL iconResource = context.getBundle().getEntry("icon/%s.svg".formatted(iconName));
try (InputStream stream = iconResource.openStream()) {
result = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
} catch (IOException e) {
logger.warn("Unable to load ressource '{}' : {}", iconResource.getPath(), e.getMessage());
}
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,10 @@ public OpenUVDiscoveryService() {

@Override
public void setThingHandler(ThingHandler handler) {
if (handler instanceof OpenUVBridgeHandler) {
OpenUVBridgeHandler localHandler = (OpenUVBridgeHandler) handler;
bridgeHandler = localHandler;
i18nProvider = localHandler.getI18nProvider();
localeProvider = localHandler.getLocaleProvider();
if (handler instanceof OpenUVBridgeHandler bridgeHandler) {
this.bridgeHandler = bridgeHandler;
this.i18nProvider = bridgeHandler.getI18nProvider();
this.localeProvider = bridgeHandler.getLocaleProvider();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
package org.openhab.binding.openuv.internal.handler;

import java.io.IOException;
import java.math.BigDecimal;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
Expand Down Expand Up @@ -58,7 +59,7 @@
*/
@NonNullByDefault
public class OpenUVBridgeHandler extends BaseBridgeHandler {
private static final String QUERY_URL = "https://api.openuv.io/api/v1/uv?lat=%s&lng=%s&alt=%s";
private static final String QUERY_URL = "https://api.openuv.io/api/v1/uv?lat=%.2f&lng=%.2f&alt=%.0f";
private static final int RECONNECT_DELAY_MIN = 5;
private static final int REQUEST_TIMEOUT_MS = (int) TimeUnit.SECONDS.toMillis(30);

Expand Down Expand Up @@ -97,27 +98,28 @@ public void initialize() {

@Override
public void dispose() {
header.clear();
freeReconnectJob();
}

@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
initiateConnexion();
return;
} else {
logger.debug("The OpenUV bridge only handles Refresh command and not '{}'", command);
}
logger.debug("The OpenUV bridge only handles Refresh command and not '{}'", command);
}

private void initiateConnexion() {
// Just checking if the provided api key is a valid one by making a fake call
getUVData("0", "0", "0");
getUVData(BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO);
}

public @Nullable OpenUVResult getUVData(String latitude, String longitude, String altitude) {
public @Nullable OpenUVResult getUVData(BigDecimal latitude, BigDecimal longitude, BigDecimal altitude) {
String statusMessage = "";
ThingStatusDetail statusDetail = ThingStatusDetail.COMMUNICATION_ERROR;
String url = String.format(QUERY_URL, latitude, longitude, altitude);
String url = QUERY_URL.formatted(latitude, longitude, altitude);
String jsonData = "";
try {
jsonData = HttpUtil.executeUrl("GET", url, header, null, null, REQUEST_TIMEOUT_MS);
Expand All @@ -133,28 +135,25 @@ private void initiateConnexion() {
}
} catch (JsonSyntaxException e) {
if (jsonData.contains("MongoError")) {
statusMessage = String.format("@text/offline.comm-error-faultly-service [ \"%d\" ]",
RECONNECT_DELAY_MIN);
statusMessage = "@text/offline.comm-error-faultly-service [ \"%d\" ]".formatted(RECONNECT_DELAY_MIN);
scheduleReconnectJob(RECONNECT_DELAY_MIN);
} else {
statusDetail = ThingStatusDetail.NONE;
statusMessage = String.format("@text/offline.invalid-json [ \"%s\" ]", url);
statusMessage = "@text/offline.invalid-json [ \"%s\" ]".formatted(url);
logger.debug("{} : {}", statusMessage, jsonData);
}
} catch (IOException e) {
statusMessage = String.format("@text/offline.comm-error-ioexception [ \"%s\",\"%d\" ]", e.getMessage(),
statusMessage = "@text/offline.comm-error-ioexception [ \"%s\",\"%d\" ]".formatted(e.getMessage(),
RECONNECT_DELAY_MIN);
scheduleReconnectJob(RECONNECT_DELAY_MIN);
} catch (OpenUVException e) {
if (e.isQuotaError()) {
LocalDateTime nextMidnight = LocalDate.now().plusDays(1).atStartOfDay().plusMinutes(2);
statusMessage = String.format("@text/offline.comm-error-quota-exceeded [ \"%s\" ]",
nextMidnight.toString());
statusMessage = "@text/offline.comm-error-quota-exceeded [ \"%s\" ]".formatted(nextMidnight.toString());
scheduleReconnectJob(Duration.between(LocalDateTime.now(), nextMidnight).toMinutes());
} else if (e.isApiKeyError()) {
if (keyVerified) {
statusMessage = String.format("@text/offline.api-key-not-recognized [ \"%d\" ]",
RECONNECT_DELAY_MIN);
statusMessage = "@text/offline.api-key-not-recognized [ \"%d\" ]".formatted(RECONNECT_DELAY_MIN);
scheduleReconnectJob(RECONNECT_DELAY_MIN);
} else {
statusDetail = ThingStatusDetail.CONFIGURATION_ERROR;
Expand Down
Loading

0 comments on commit 2ce9ede

Please sign in to comment.