Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Openuv] Provide UV Index iconset #15191

Merged
merged 3 commits into from
Jul 13, 2023
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
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);
lolodomo marked this conversation as resolved.
Show resolved Hide resolved

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