From 9d45a2628dd7f061c812382e5227c281edb71ffb Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Mon, 10 May 2021 20:02:18 +0200 Subject: [PATCH 01/13] initial commit Signed-off-by: Bernd Weymann --- bundles/org.openhab.binding.ems/NOTICE | 13 + bundles/org.openhab.binding.ems/README.md | 56 + bundles/org.openhab.binding.ems/pom.xml | 17 + .../src/main/feature/feature.xml | 9 + .../binding/ems/dto/DailyForecast.java | 25 + .../binding/ems/dto/DayTemperature.java | 10 + .../openhab/binding/ems/dto/HourForecast.java | 21 + .../org/openhab/binding/ems/dto/HourRain.java | 8 + .../binding/ems/dto/OnecallWeather.java | 13 + .../binding/ems/dto/WeatherCurrent.java | 21 + .../binding/ems/dto/WeatherDescription.java | 8 + .../ems/internal/EMSBindingConstants.java | 34 + .../ems/internal/EMSConfiguration.java | 22 + .../ems/internal/EMSHandlerFactory.java | 65 + .../ems/internal/handler/EMSHandler.java | 158 ++ .../ems/internal/handler/WeatherForecast.java | 95 + .../openhab/binding/ems/utils/Constants.java | 7 + .../openhab/binding/ems/utils/Formulas.java | 146 ++ .../main/resources/OH-INF/binding/binding.xml | 9 + .../resources/OH-INF/i18n/ems_xx.properties | 21 + .../resources/OH-INF/thing/thing-types.xml | 58 + .../org/openhab/binding/ems/TestFormulas.java | 151 ++ bundles/org.openhab.binding.ems/weather.json | 1787 ++++++++++++++++ bundles/org.openhab.binding.ems/weather2.json | 1801 +++++++++++++++++ bundles/org.openhab.binding.ems/weather3.json | 1777 ++++++++++++++++ 25 files changed, 6332 insertions(+) create mode 100644 bundles/org.openhab.binding.ems/NOTICE create mode 100644 bundles/org.openhab.binding.ems/README.md create mode 100644 bundles/org.openhab.binding.ems/pom.xml create mode 100644 bundles/org.openhab.binding.ems/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/DailyForecast.java create mode 100644 bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/DayTemperature.java create mode 100644 bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/HourForecast.java create mode 100644 bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/HourRain.java create mode 100644 bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/OnecallWeather.java create mode 100644 bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/WeatherCurrent.java create mode 100644 bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/WeatherDescription.java create mode 100644 bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/EMSBindingConstants.java create mode 100644 bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/EMSConfiguration.java create mode 100644 bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/EMSHandlerFactory.java create mode 100644 bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/handler/EMSHandler.java create mode 100644 bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/handler/WeatherForecast.java create mode 100644 bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/utils/Constants.java create mode 100644 bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/utils/Formulas.java create mode 100644 bundles/org.openhab.binding.ems/src/main/resources/OH-INF/binding/binding.xml create mode 100644 bundles/org.openhab.binding.ems/src/main/resources/OH-INF/i18n/ems_xx.properties create mode 100644 bundles/org.openhab.binding.ems/src/main/resources/OH-INF/thing/thing-types.xml create mode 100644 bundles/org.openhab.binding.ems/src/test/java/org/openhab/binding/ems/TestFormulas.java create mode 100644 bundles/org.openhab.binding.ems/weather.json create mode 100644 bundles/org.openhab.binding.ems/weather2.json create mode 100644 bundles/org.openhab.binding.ems/weather3.json diff --git a/bundles/org.openhab.binding.ems/NOTICE b/bundles/org.openhab.binding.ems/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.ems/NOTICE @@ -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 diff --git a/bundles/org.openhab.binding.ems/README.md b/bundles/org.openhab.binding.ems/README.md new file mode 100644 index 0000000000000..1a3861729841f --- /dev/null +++ b/bundles/org.openhab.binding.ems/README.md @@ -0,0 +1,56 @@ +# EMS Binding + +_Give some details about what this binding is meant for - a protocol, system, specific device._ + +_If possible, provide some resources like pictures, a video, etc. to give an impression of what can be done with this binding. You can place such resources into a `doc` folder next to this README.md._ + +## Supported Things + +_Please describe the different supported things / devices within this section._ +_Which different types are supported, which models were tested etc.?_ +_Note that it is planned to generate some part of this based on the XML files within ```src/main/resources/OH-INF/thing``` of your binding._ + +## Discovery + +_Describe the available auto-discovery features here. Mention for what it works and what needs to be kept in mind when using it._ + +## Binding Configuration + +_If your binding requires or supports general configuration settings, please create a folder ```cfg``` and place the configuration file ```.cfg``` inside it. In this section, you should link to this file and provide some information about the options. The file could e.g. look like:_ + +``` +# Configuration for the EMS Binding +# +# Default secret key for the pairing of the EMS Thing. +# It has to be between 10-40 (alphanumeric) characters. +# This may be changed by the user for security reasons. +secret=openHABSecret +``` + +_Note that it is planned to generate some part of this based on the information that is available within ```src/main/resources/OH-INF/binding``` of your binding._ + +_If your binding does not offer any generic configurations, you can remove this section completely._ + +## Thing Configuration + +_Describe what is needed to manually configure a thing, either through the UI or via a thing-file. This should be mainly about its mandatory and optional configuration parameters. A short example entry for a thing file can help!_ + +_Note that it is planned to generate some part of this based on the XML files within ```src/main/resources/OH-INF/thing``` of your binding._ + +## Channels + +_Here you should provide information about available channel types, what their meaning is and how they can be used._ + +_Note that it is planned to generate some part of this based on the XML files within ```src/main/resources/OH-INF/thing``` of your binding._ + +| channel | type | description | +|----------|--------|------------------------------| +| control | Switch | This is the control channel | + +## Full Example + +_Provide a full usage example based on textual configuration files (*.things, *.items, *.sitemap)._ + +## Any custom content here! + +_Feel free to add additional sections for whatever you think should also be mentioned about your binding!_ diff --git a/bundles/org.openhab.binding.ems/pom.xml b/bundles/org.openhab.binding.ems/pom.xml new file mode 100644 index 0000000000000..607ddbee7b035 --- /dev/null +++ b/bundles/org.openhab.binding.ems/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.1.0-SNAPSHOT + + + org.openhab.binding.ems + + openHAB Add-ons :: Bundles :: EMS Binding + + diff --git a/bundles/org.openhab.binding.ems/src/main/feature/feature.xml b/bundles/org.openhab.binding.ems/src/main/feature/feature.xml new file mode 100644 index 0000000000000..b4dce3bcfb62f --- /dev/null +++ b/bundles/org.openhab.binding.ems/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.ems/${project.version} + + diff --git a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/DailyForecast.java b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/DailyForecast.java new file mode 100644 index 0000000000000..f97537761e1e9 --- /dev/null +++ b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/DailyForecast.java @@ -0,0 +1,25 @@ +package org.openhab.binding.ems.dto; + +import java.util.List; + +public class DailyForecast { + public int dt; // ":1620475200, + public int sunrise; // ":1620449957, + public int sunset; // ":1620504821, + public int moonrise; // ":1620447540, + public int moonset; // ":1620492060, + public double moon_phase; // ":0.9, + public DayTemperature temp; // ":{"day":284.62,"min":283.83,"max":285.33,"night":284.4,"eve":284.54,"morn":284.62}, + public DayTemperature feels_like; // ":{"day":284.07,"night":284.25,"eve":283.88,"morn":284.25}, + public int pressure; // ":993, + public int humidity; // ":86, + public double dew_point; // ":282.44, + public double wind_speed; // ":14.63, + public int wind_deg; // ":120, + public double wind_gust; // ":19.66, + public List weather; // ":[{"id":501,"main":"Rain","description":"moderate rain","icon":"10d"}], + public int clouds; // ":25, + public double pop; // ":1, + public double rain; // ":11.17, + public double uvi; // ":6.25 +} diff --git a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/DayTemperature.java b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/DayTemperature.java new file mode 100644 index 0000000000000..0febdd5920df2 --- /dev/null +++ b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/DayTemperature.java @@ -0,0 +1,10 @@ +package org.openhab.binding.ems.dto; + +public class DayTemperature { + public double day; // ":284.62, + public double min; // ":283.83, + public double max; // ":285.33, + public double night; // ":284.4, + public double eve; // ":284.54, + public double morn; // ":284.62 +} diff --git a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/HourForecast.java b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/HourForecast.java new file mode 100644 index 0000000000000..b8a6126d84f48 --- /dev/null +++ b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/HourForecast.java @@ -0,0 +1,21 @@ +package org.openhab.binding.ems.dto; + +import java.util.List; + +public class HourForecast { + public int dt; // ":1620507600, + public double temp; // ":284.43, + public double feels_like; // ":283.73, + public int pressure; // ":987, + public int humidity; // ":81, + public double dew_point; // ":281.29, + public double uvi; // ":0, + public int clouds; // ":65, + public int visibility; // ":10000, + public double wind_speed; // ":13.78, + public int wind_deg; // ":190, + public double wind_gust; // ":17.82, + public List weather; // ":[{"id":500,"main":"Rain","description":"light rain","icon":"10n"}], + public double pop; // ":0.68, + public HourRain rain; // ":{"1h":0.23} +} diff --git a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/HourRain.java b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/HourRain.java new file mode 100644 index 0000000000000..de467a0f408f6 --- /dev/null +++ b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/HourRain.java @@ -0,0 +1,8 @@ +package org.openhab.binding.ems.dto; + +import com.google.gson.annotations.SerializedName; + +public class HourRain { + @SerializedName("1h") + public double oneh; // ":0.23 +} diff --git a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/OnecallWeather.java b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/OnecallWeather.java new file mode 100644 index 0000000000000..23d22c7e67f2c --- /dev/null +++ b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/OnecallWeather.java @@ -0,0 +1,13 @@ +package org.openhab.binding.ems.dto; + +import java.util.List; + +public class OnecallWeather { + public double lat; // ": 51.44, + public double lon; // ": -10, + public String timezone; // ": "Europe/Dublin", + public int timezone_offset; // ": 3600, + public WeatherCurrent current; + public List hourly; + public List daily; +} diff --git a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/WeatherCurrent.java b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/WeatherCurrent.java new file mode 100644 index 0000000000000..36c4088d680c2 --- /dev/null +++ b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/WeatherCurrent.java @@ -0,0 +1,21 @@ +package org.openhab.binding.ems.dto; + +import java.util.List; + +public class WeatherCurrent { + public int dt; // ": 1620509255, + public int sunrisedt; // ": 1620449957, + public int sunsetdt; // ": 1620504821, + public double tempdt; // ": 284.43, + public double feels_likedt; // ": 283.73, + public int pressuredt; // ": 987, + public int humiditydt; // ": 81, + public double dew_pointdt; // ": 281.29, + public int uvidt; // ": 0, + public int cloudsdt; // ": 65, + public int visibilitydt; // ": 10000, + public double wind_speeddt; // ": 13.78, + public int wind_degdt; // ": 190, + public double wind_gustdt; // ": 17.82, + public List weather; +} diff --git a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/WeatherDescription.java b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/WeatherDescription.java new file mode 100644 index 0000000000000..5978a3c45358a --- /dev/null +++ b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/WeatherDescription.java @@ -0,0 +1,8 @@ +package org.openhab.binding.ems.dto; + +public class WeatherDescription { + public int id; // ": 803, + public String main; // ": "Clouds", + public String description; // ": "broken clouds", + public String icon; // ": "04n" +} diff --git a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/EMSBindingConstants.java b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/EMSBindingConstants.java new file mode 100644 index 0000000000000..59486f28c6f2c --- /dev/null +++ b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/EMSBindingConstants.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2021 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.ems.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link EMSBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class EMSBindingConstants { + + private static final String BINDING_ID = "ems"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_EMS = new ThingTypeUID(BINDING_ID, "EMS"); + + // List of all Channel ids + public static final String CHANNEL_1 = "channel1"; +} diff --git a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/EMSConfiguration.java b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/EMSConfiguration.java new file mode 100644 index 0000000000000..7acd4352396ae --- /dev/null +++ b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/EMSConfiguration.java @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2010-2021 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.ems.internal; + +/** + * The {@link EMSConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Bernd Weymann - Initial contribution + */ +public class EMSConfiguration { + public String owmApiKey; +} diff --git a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/EMSHandlerFactory.java b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/EMSHandlerFactory.java new file mode 100644 index 0000000000000..886148e9df469 --- /dev/null +++ b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/EMSHandlerFactory.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2010-2021 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.ems.internal; + +import static org.openhab.binding.ems.internal.EMSBindingConstants.THING_TYPE_EMS; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.ems.internal.handler.EMSHandler; +import org.openhab.core.i18n.LocationProvider; +import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link EMSHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.ems", service = ThingHandlerFactory.class) +public class EMSHandlerFactory extends BaseThingHandlerFactory { + + LocationProvider locationProvider; + + public EMSHandlerFactory(final @Reference LocationProvider lp, final @Reference TimeZoneProvider timeZoneProvider) { + locationProvider = lp; + } + + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_EMS); + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_EMS.equals(thingTypeUID)) { + return new EMSHandler(thing, locationProvider.getLocation()); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/handler/EMSHandler.java b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/handler/EMSHandler.java new file mode 100644 index 0000000000000..a84e5098a7396 --- /dev/null +++ b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/handler/EMSHandler.java @@ -0,0 +1,158 @@ +/** + * Copyright (c) 2010-2021 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.ems.internal.handler; + +import static org.openhab.binding.ems.internal.EMSBindingConstants.CHANNEL_1; + +import java.util.Calendar; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.ems.internal.EMSConfiguration; +import org.openhab.binding.ems.utils.Formulas; +import org.openhab.core.library.types.PointType; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link EMSHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class EMSHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(EMSHandler.class); + // Granularity of forecast in minutes + private final static int FORECAST_GRANULARITY_MIN = 5; + + private @Nullable EMSConfiguration config; + private Optional lastUpdate = Optional.empty(); + private Optional wf = Optional.empty(); + private double latitude = 0; + private double longitude = 0; + + public EMSHandler(Thing thing, @Nullable PointType pt) { + super(thing); + if (pt != null) { + latitude = pt.getLatitude().doubleValue(); + longitude = pt.getLongitude().doubleValue(); + } else { + logger.warn("No Location given - forecast will not work without havin location!"); + } + + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (CHANNEL_1.equals(channelUID.getId())) { + if (command instanceof RefreshType) { + // TODO: handle data refresh + } + + // TODO: handle command + + // Note: if communication with thing fails for some reason, + // indicate that by setting the status with detail information: + // updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + // "Could not control device at IP address x.x.x.x"); + } + } + + @Override + public void initialize() { + setConfiguration(getConfigAs(EMSConfiguration.class)); + updateStatus(ThingStatus.ONLINE); + wf = Optional.of(new WeatherForecast(latitude, longitude, config.owmApiKey)); + scheduler.scheduleAtFixedRate(this::update, 0, 5, TimeUnit.MINUTES); + } + + public void setConfiguration(EMSConfiguration c) { + config = c; + } + + public void update() { + Calendar update = Calendar.getInstance(); + if (newHour(lastUpdate, update)) { + + } + if (newDay(lastUpdate, update)) { + generatePrediction(); + } + lastUpdate = Optional.of(update); + } + + public void generatePrediction() { + wf = Optional.of(new WeatherForecast(latitude, longitude, config.owmApiKey)); + Calendar date = Calendar.getInstance(); + for (int days = 0; days < 7; days++) { + int observationDay = date.get(Calendar.DAY_OF_MONTH); + String observationDaate = (date.get(Calendar.DAY_OF_MONTH) + 1) + "." + (date.get(Calendar.MONTH) + 1) + "." + + date.get(Calendar.YEAR); + double dailyProduction = 0; + int dCounter = 0; + double dCloduiness = 0; + while (observationDay == date.get(Calendar.DAY_OF_MONTH)) { + int forecast_hour = date.get(Calendar.HOUR_OF_DAY); + int forecast_minute = date.get(Calendar.MINUTE); + double sunHeight = Formulas.round(Formulas.sunPositionDIN(date.get(Calendar.YEAR), + date.get(Calendar.MONTH) + 1, date.get(Calendar.DAY_OF_MONTH) + 1, forecast_hour, + forecast_minute * FORECAST_GRANULARITY_MIN, 0, latitude, 10, 2), 1); + if (sunHeight > 0) { + int cloudiness = wf.get().getCloudiness(observationDay, forecast_hour); + // logger.info("Cloudiness on {} {}: {}", observationDaate, forecast_hour, cloudiness); + double radiationInfo = Formulas.getRadiationInfo(Formulas.getCalendar(date.get(Calendar.YEAR), + date.get(Calendar.MONTH) + 1, date.get(Calendar.DAY_OF_MONTH) + 1, forecast_hour, + forecast_minute * FORECAST_GRANULARITY_MIN), sunHeight, latitude); + double adjustedRadiationInfo = radiationInfo * (100 - cloudiness) / 100; + double production = Formulas + .round(9.75 * adjustedRadiationInfo / 1000 * FORECAST_GRANULARITY_MIN / 60, 3); + double cloudProduction = Formulas.round(production * 100 - cloudiness / 100, 3); + if (cloudiness > 20 && cloudiness < 80) { + logger.info("Cloudiness: {} Factor {}", cloudiness, Double.valueOf(cloudiness) / 100.0); + } + // logger.info("Best Production: {} Cloud Production {}", production, + // Double.valueOf(cloudiness) / 100.0); + dailyProduction += production;// * (100 - cloudiness) / 100; + dCloduiness += cloudiness; + dCounter++; + } + date.add(Calendar.MINUTE, FORECAST_GRANULARITY_MIN); + } + // updateState(new ChannelUID("pv-prediction-today"), + // QuantityType.valueOf(dailyProduction, Units.KILOWATT_HOUR)); + dailyProduction = Formulas.round(dailyProduction, 0); + dCloduiness = Formulas.round(dCloduiness / dCounter, 0); + logger.info("Prediction for {} is {} with ~ {}% cloudiness", observationDaate, dailyProduction, + dCloduiness); + } + } + + private boolean newHour(Optional lastUpdate2, Calendar update) { + return lastUpdate2.isEmpty() ? true : lastUpdate2.get().get(Calendar.HOUR) != update.get(Calendar.HOUR); + } + + private boolean newDay(Optional lastUpdate2, Calendar update) { + return lastUpdate2.isEmpty() ? true + : lastUpdate2.get().get(Calendar.DAY_OF_YEAR) != update.get(Calendar.DAY_OF_YEAR); + } +} diff --git a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/handler/WeatherForecast.java b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/handler/WeatherForecast.java new file mode 100644 index 0000000000000..ac7bfdfcf3cf4 --- /dev/null +++ b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/handler/WeatherForecast.java @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2010-2021 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.ems.internal.handler; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Calendar; +import java.util.Iterator; +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.ems.dto.DailyForecast; +import org.openhab.binding.ems.dto.HourForecast; +import org.openhab.binding.ems.dto.OnecallWeather; +import org.openhab.binding.ems.utils.Constants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link WeatherForecast} handles forecast data + * + * @author Bernd Weymann - Initial contribution + */ +@NonNullByDefault +public class WeatherForecast { + + private final Logger logger = LoggerFactory.getLogger(WeatherForecast.class); + private Optional ocw = Optional.empty(); + + public WeatherForecast(double lat, double lon, String apiKey) { + try { + URL weatherApi = new URL( + "https://api.openweathermap.org/data/2.5/onecall?lat=" + lat + "&lon=" + lon + "&appid=" + apiKey); + HttpURLConnection con = (HttpURLConnection) weatherApi.openConnection(); + con.setRequestMethod("GET"); + BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); + String inputLine; + StringBuffer content = new StringBuffer(); + while ((inputLine = in.readLine()) != null) { + content.append(inputLine); + } + in.close(); + System.out.println(content.toString()); + OnecallWeather check = Constants.GSON.fromJson(content.toString(), OnecallWeather.class); + if (check != null) { + ocw = Optional.of(check); + } + } catch (MalformedURLException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + public int getCloudiness(int day, int hour) { + if (ocw.isPresent()) { + // check if hourly forecast is present + for (Iterator iterator = ocw.get().hourly.iterator(); iterator.hasNext();) { + HourForecast hf = (HourForecast) iterator.next(); + Calendar c = Calendar.getInstance(); + c.setTimeInMillis((long) hf.dt * 1000); + if (c.get(Calendar.DAY_OF_MONTH) == day && c.get(Calendar.HOUR_OF_DAY) == hour) { + return hf.clouds; + } + } + // else return daily + for (Iterator iterator = ocw.get().daily.iterator(); iterator.hasNext();) { + DailyForecast df = (DailyForecast) iterator.next(); + Calendar c = Calendar.getInstance(); + c.setTimeInMillis((long) df.dt * 1000); + if (c.get(Calendar.DAY_OF_MONTH) == day) { + return df.clouds; + } + } + } + // logger.info("No Cloudiness found for day {} hour {}", day, hour); + return -1; + } +} diff --git a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/utils/Constants.java b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/utils/Constants.java new file mode 100644 index 0000000000000..8734aa0bcc050 --- /dev/null +++ b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/utils/Constants.java @@ -0,0 +1,7 @@ +package org.openhab.binding.ems.utils; + +import com.google.gson.Gson; + +public class Constants { + public static final Gson GSON = new Gson(); +} diff --git a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/utils/Formulas.java b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/utils/Formulas.java new file mode 100644 index 0000000000000..ed349b3182074 --- /dev/null +++ b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/utils/Formulas.java @@ -0,0 +1,146 @@ +package org.openhab.binding.ems.utils; + +import java.util.Calendar; + +public class Formulas { + + private static final double SC = 1367; // Solar constant in W/m² + public static final double DEG2RAD = Math.PI / 180; + public static final double RAD2DEG = 180. / Math.PI; + + public static Calendar getCalendar(int year, int month, int day, int hour, int minute) { + Calendar c = Calendar.getInstance(); + c.set(year, month, day, hour, minute); + return c; + } + + /** + * https://www.volker-quaschning.de/datserv/sunpos/sunpos.js + * https://wiki.energie-m.de/Sonnenstandsberechnung + * (GMT=0, MEZ=1, MESZ(Sommerzeit)=2) + */ + public static double sunPositionDIN(int year, int month, int day, int hour, int min, int sec, double lat, + double lon, int timezone) { + double J, J2; + double Zgl, MOZ, WOZ, w; + double decl; + double sunaz, sunhi; + double asinGs; + double acosAs; + + J2 = 365; + if (year % 4 == 0) { + J2++; + } + J = dayOfYear(year, month, day); + MOZ = hour + 1.0 / 60 * min + 1.0 / 3600 * sec - timezone + 1; + MOZ = MOZ - 4 * (15 - lon) / 60; + J = J * 360 / J2 + MOZ / 24; + decl = 0.3948 - 23.2559 * Math.cos(rad(J + 9.1)) - 0.3915 * Math.cos(rad(2 * J + 5.4)) + - 0.1764 * Math.cos(rad(3 * J + 26.0)); + Zgl = 0.0066 + 7.3525 * Math.cos(rad(J + 85.9)) + 9.9359 * Math.cos(rad(2 * J + 108.9)) + + 0.3387 * Math.cos(rad(3 * J + 105.2)); + WOZ = MOZ + Zgl / 60; + w = (12 - WOZ) * 15; + asinGs = Math.cos(rad(w)) * Math.cos(rad(lat)) * Math.cos(rad(decl)) + Math.sin(rad(lat)) * Math.sin(rad(decl)); + if (asinGs > 1) { + asinGs = 1; + } + if (asinGs < -1) { + asinGs = -1; + } + sunhi = grad(Math.asin(asinGs)); + acosAs = (Math.sin(rad(sunhi)) * Math.sin(rad(lat)) - Math.sin(rad(decl))) + / (Math.cos(rad(sunhi)) * Math.cos(rad(lat))); + if (acosAs > 1) { + acosAs = 1; + } + if (acosAs < -1) { + acosAs = -1; + } + sunaz = grad(Math.acos(acosAs)); + if ((WOZ > 12) || (WOZ < 0)) { + sunaz = 180 + sunaz; + } else { + sunaz = 180 - sunaz; + } + ; + double azimuth = sunaz;// * 1000) / 1000; + double height = sunhi;// * 1000) / 1000; + return round(height, 3); + } + + public static int dayOfYear(int year, int month, int day) { + int x; + int[] monthday = new int[] { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 }; + + x = monthday[month - 1] + day; + if ((year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0)) && (month > 2)) { + x++; + } + return x; + } + + public static double rad(double grad) { + return (grad * Math.PI / 180); + } + + public static double grad(double rad) { + return (rad * 180 / Math.PI); + } + + public static double airMass(double height) { + if (height > 0) { + double airmass = 1000 / Math.sin(rad(height)) / 1000; + double zenithangle = Math.round((90 - height) * 1000) / 1000; + return round(airmass, 3); + } + return 0; + } + + public static double round(double value, int places) { + if (places < 0) { + throw new IllegalArgumentException(); + } + + long factor = (long) Math.pow(10, places); + long tmp = Math.round(value * factor); + return (double) tmp / factor; + } + + /** + * https://en.wikipedia.org/wiki/Air_mass_(solar_energy) + */ + public static double solarIntensity(double airMass) { + double si = 1.1 * 1367 * (Math.pow(0.7, Math.pow(airMass, 0.678))); + return round(si, 3); + // {\displaystyle I=1.1\times I_{\mathrm {o} }\times 0.7^{(AM^{0.678})}\,} + } + + /** + * Calculates sun radiation data. + */ + public static double getRadiationInfo(Calendar calendar, double elevation, Double altitude) { + double sinAlpha = Math.sin(DEG2RAD * elevation); + + int dayOfYear = calendar.get(Calendar.DAY_OF_YEAR); + int daysInYear = calendar.getActualMaximum(Calendar.DAY_OF_YEAR); + + // Direct Solar Radiation (in W/m²) at the atmosphere entry + // At sunrise/sunset - calculations limits are reached + double rOut = (elevation > 3) ? SC * (0.034 * Math.cos(DEG2RAD * (360 * dayOfYear / daysInYear)) + 1) : 0; + double altitudeRatio = (altitude != null) ? 1 / Math.pow((1 - (6.5 / 288) * (altitude / 1000.0)), 5.256) : 1; + double m = (Math.sqrt(1229 + Math.pow(614 * sinAlpha, 2)) - 614 * sinAlpha) * altitudeRatio; + + // Direct radiation after atmospheric layer + // 0.6 = Coefficient de transmissivité + double rDir = rOut * Math.pow(0.6, m) * sinAlpha; + + // Diffuse Radiation + double rDiff = rOut * (0.271 - 0.294 * Math.pow(0.6, m)) * sinAlpha; + double rTot = rDir + rDiff; + + return rTot; + } + +} diff --git a/bundles/org.openhab.binding.ems/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.ems/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 0000000000000..6ac7b6aeeb658 --- /dev/null +++ b/bundles/org.openhab.binding.ems/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,9 @@ + + + + EMS Binding + This is the binding for EMS. + + diff --git a/bundles/org.openhab.binding.ems/src/main/resources/OH-INF/i18n/ems_xx.properties b/bundles/org.openhab.binding.ems/src/main/resources/OH-INF/i18n/ems_xx.properties new file mode 100644 index 0000000000000..85bb996658174 --- /dev/null +++ b/bundles/org.openhab.binding.ems/src/main/resources/OH-INF/i18n/ems_xx.properties @@ -0,0 +1,21 @@ +# FIXME: please substitute the xx with a proper locale, ie. de +# FIXME: please do not add the file to the repo if you add or change no content +# binding +binding.ems.name = +binding.ems.description = + +# thing types +thing-type.ems.sample.label = +thing-type.ems.sample.description = + +# thing type config description +thing-type.config.ems.sample.hostname.label = +thing-type.config.ems.sample.hostname.description = +thing-type.config.ems.sample.password.label = +thing-type.config.ems.sample.password.description = +thing-type.config.ems.sample.refreshInterval.label = +thing-type.config.ems.sample.refreshInterval.description = + +# channel types +channel-type.ems.sample-channel.label = +channel-type.ems.sample-channel.description = diff --git a/bundles/org.openhab.binding.ems/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.ems/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..ef560ab05d6e0 --- /dev/null +++ b/bundles/org.openhab.binding.ems/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,58 @@ + + + + + + + Sample thing for EMS Binding + + + + + + + + + + + + + + + + + + + + + + + + + + + network-address + + Hostname or IP address of the device + + + password + + Passwort to access the device + + + + Interval the device is polled in sec. + + + + + + + Number:Temperature + + Sample channel for EMS Binding + + diff --git a/bundles/org.openhab.binding.ems/src/test/java/org/openhab/binding/ems/TestFormulas.java b/bundles/org.openhab.binding.ems/src/test/java/org/openhab/binding/ems/TestFormulas.java new file mode 100644 index 0000000000000..7b5fe726ea121 --- /dev/null +++ b/bundles/org.openhab.binding.ems/src/test/java/org/openhab/binding/ems/TestFormulas.java @@ -0,0 +1,151 @@ +package org.openhab.binding.ems; + +import static org.mockito.Mockito.*; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Calendar; + +import org.junit.jupiter.api.Test; +import org.openhab.binding.ems.dto.OnecallWeather; +import org.openhab.binding.ems.internal.EMSConfiguration; +import org.openhab.binding.ems.internal.handler.EMSHandler; +import org.openhab.binding.ems.utils.Constants; +import org.openhab.binding.ems.utils.Formulas; +import org.openhab.core.library.types.PointType; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingUID; + +class TestFormulas { + + @Test + void test() { + int minuteGRanularity = 5; + int month = 5; + double totalProduction = 0; + for (int i = 0; i < 24; i++) { + for (int j = 0; j * minuteGRanularity < 60; j++) { + double sunHeight = Formulas + .round(Formulas.sunPositionDIN(2021, month, 8, i, j * minuteGRanularity, 0, 50, 10, 2), 1); + double airMass = Formulas.round(Formulas.airMass(sunHeight), 4); + double intensity = Formulas.round(Formulas.solarIntensity(airMass), 1); + double winkel = 90 - sunHeight + 15; + double realIntensity = Math.sin(Math.toRadians(winkel)); + double production = Formulas + .round(9.75 + * Formulas.getRadiationInfo( + Formulas.getCalendar(2021, month, 8, i, j * minuteGRanularity), sunHeight, 50.0) + / 1000 * minuteGRanularity / 60, 3); + if (sunHeight > 0) { + totalProduction += production; + System.out.println("Hour: " + i + " Height: " + sunHeight + " Air Mass: " + airMass + " Intensity: " + + intensity + " Winkel: " + winkel + " Production: " + production); + System.out.println("Rad: " + + Formulas.getRadiationInfo(Formulas.getCalendar(2021, 5, 8, i, 30), sunHeight, 50.0)); + } + } + } + System.out.println(totalProduction); + } + + @Test + public void test2() { + Calendar date = Calendar.getInstance(); + int minuteGranularity = 5; + double totalProduction = 0; + for (int i = 0; i < 24; i++) { + for (int j = 0; j * minuteGranularity < 60; j++) { + double sunHeight = Formulas + .round(Formulas.sunPositionDIN(date.get(Calendar.YEAR), date.get(Calendar.MONTH) + 1, + date.get(Calendar.DAY_OF_MONTH) + 1, i, j * minuteGranularity, 0, 50, 10, 2), 1); + double production = Formulas + .round(9.75 + * Formulas.getRadiationInfo( + Formulas.getCalendar(date.get(Calendar.YEAR), date.get(Calendar.MONTH) + 1, + date.get(Calendar.DAY_OF_MONTH) + 1, i, j * minuteGranularity), + sunHeight, 50.0) + / 1000 * minuteGranularity / 60, 3); + if (sunHeight > 0) { + totalProduction += production; + } + } + } + System.out.println(totalProduction); + } + + @Test + public void testWeather() { + try { + URL weatherApi = new URL( + "https://api.openweathermap.org/data/2.5/onecall?lat=51.44&lon=-10.0&appid=7c82a05c28361abc8ab90b9f0faf18fa"); + // URL weatherApi = new URL( + // "https://api.openweathermap.org/data/2.5/weather?q=Wetzlar&appid=7c82a05c28361abc8ab90b9f0faf18fa"); + HttpURLConnection con = (HttpURLConnection) weatherApi.openConnection(); + con.setRequestMethod("GET"); + BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); + String inputLine; + StringBuffer content = new StringBuffer(); + while ((inputLine = in.readLine()) != null) { + content.append(inputLine); + } + in.close(); + System.out.println(content.toString()); + OnecallWeather ocw = Constants.GSON.fromJson(content.toString(), OnecallWeather.class); + System.out.println(ocw.hourly.size()); + System.out.println(ocw.daily.size()); + } catch (MalformedURLException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + @Test + public void testTimeConversion() { + long ut1 = Instant.now().getEpochSecond(); + System.out.println(ut1); + + System.out.println(Instant.ofEpochMilli(ut1)); + java.util.Date time = new java.util.Date(ut1); + System.out.println(time); + + java.util.Date time2 = new java.util.Date(1620507600); + System.out.println(time2); + + long epoch = Instant.now().toEpochMilli(); + System.out.println(epoch); + epoch = (long) 1620507600 * 1000; + System.out.println(epoch); + + LocalDate ld = Instant.ofEpochMilli(epoch).atZone(ZoneId.systemDefault()).toLocalDate(); + System.out.println(ld); + + LocalDateTime ldt = Instant.ofEpochMilli(epoch).atZone(ZoneId.systemDefault()).toLocalDateTime(); + System.out.println(ldt); + + java.util.Date time3 = new java.util.Date((long) 1620633600 * 1000); + System.out.println(time3); + } + + @Test + public void testEMSHandler() { + PointType pt = PointType.valueOf("50.55,8.43"); + Thing thing = mock(Thing.class); + when(thing.getUID()).thenReturn(new ThingUID("testbinding", "test")); + EMSHandler ems = new EMSHandler(thing, pt); + EMSConfiguration c = new EMSConfiguration(); + c.owmApiKey = "7c82a05c28361abc8ab90b9f0faf18fa"; + ems.setConfiguration(c); + ems.generatePrediction(); + } +} diff --git a/bundles/org.openhab.binding.ems/weather.json b/bundles/org.openhab.binding.ems/weather.json new file mode 100644 index 0000000000000..da62df1818f50 --- /dev/null +++ b/bundles/org.openhab.binding.ems/weather.json @@ -0,0 +1,1787 @@ +{ + "lat": 51.44, + "lon": -10, + "timezone": "Europe/Dublin", + "timezone_offset": 3600, + "current": { + "dt": 1620509255, + "sunrise": 1620449957, + "sunset": 1620504821, + "temp": 284.43, + "feels_like": 283.73, + "pressure": 987, + "humidity": 81, + "dew_point": 281.29, + "uvi": 0, + "clouds": 65, + "visibility": 10000, + "wind_speed": 13.78, + "wind_deg": 190, + "wind_gust": 17.82, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04n" + } + ] + }, + "minutely": [ + { + "dt": 1620509280, + "precipitation": 0 + }, + { + "dt": 1620509340, + "precipitation": 0 + }, + { + "dt": 1620509400, + "precipitation": 0 + }, + { + "dt": 1620509460, + "precipitation": 0 + }, + { + "dt": 1620509520, + "precipitation": 0 + }, + { + "dt": 1620509580, + "precipitation": 0 + }, + { + "dt": 1620509640, + "precipitation": 0 + }, + { + "dt": 1620509700, + "precipitation": 0 + }, + { + "dt": 1620509760, + "precipitation": 0 + }, + { + "dt": 1620509820, + "precipitation": 0 + }, + { + "dt": 1620509880, + "precipitation": 0 + }, + { + "dt": 1620509940, + "precipitation": 0 + }, + { + "dt": 1620510000, + "precipitation": 0 + }, + { + "dt": 1620510060, + "precipitation": 0 + }, + { + "dt": 1620510120, + "precipitation": 0 + }, + { + "dt": 1620510180, + "precipitation": 0 + }, + { + "dt": 1620510240, + "precipitation": 0 + }, + { + "dt": 1620510300, + "precipitation": 0 + }, + { + "dt": 1620510360, + "precipitation": 0 + }, + { + "dt": 1620510420, + "precipitation": 0 + }, + { + "dt": 1620510480, + "precipitation": 0 + }, + { + "dt": 1620510540, + "precipitation": 0 + }, + { + "dt": 1620510600, + "precipitation": 0 + }, + { + "dt": 1620510660, + "precipitation": 0 + }, + { + "dt": 1620510720, + "precipitation": 0 + }, + { + "dt": 1620510780, + "precipitation": 0 + }, + { + "dt": 1620510840, + "precipitation": 0 + }, + { + "dt": 1620510900, + "precipitation": 0 + }, + { + "dt": 1620510960, + "precipitation": 0 + }, + { + "dt": 1620511020, + "precipitation": 0 + }, + { + "dt": 1620511080, + "precipitation": 0 + }, + { + "dt": 1620511140, + "precipitation": 0 + }, + { + "dt": 1620511200, + "precipitation": 0 + }, + { + "dt": 1620511260, + "precipitation": 0 + }, + { + "dt": 1620511320, + "precipitation": 0 + }, + { + "dt": 1620511380, + "precipitation": 0 + }, + { + "dt": 1620511440, + "precipitation": 0 + }, + { + "dt": 1620511500, + "precipitation": 0 + }, + { + "dt": 1620511560, + "precipitation": 0 + }, + { + "dt": 1620511620, + "precipitation": 0 + }, + { + "dt": 1620511680, + "precipitation": 0 + }, + { + "dt": 1620511740, + "precipitation": 0 + }, + { + "dt": 1620511800, + "precipitation": 0 + }, + { + "dt": 1620511860, + "precipitation": 0 + }, + { + "dt": 1620511920, + "precipitation": 0 + }, + { + "dt": 1620511980, + "precipitation": 0 + }, + { + "dt": 1620512040, + "precipitation": 0 + }, + { + "dt": 1620512100, + "precipitation": 0 + }, + { + "dt": 1620512160, + "precipitation": 0 + }, + { + "dt": 1620512220, + "precipitation": 0 + }, + { + "dt": 1620512280, + "precipitation": 0 + }, + { + "dt": 1620512340, + "precipitation": 0 + }, + { + "dt": 1620512400, + "precipitation": 0 + }, + { + "dt": 1620512460, + "precipitation": 0 + }, + { + "dt": 1620512520, + "precipitation": 0 + }, + { + "dt": 1620512580, + "precipitation": 0 + }, + { + "dt": 1620512640, + "precipitation": 0 + }, + { + "dt": 1620512700, + "precipitation": 0 + }, + { + "dt": 1620512760, + "precipitation": 0 + }, + { + "dt": 1620512820, + "precipitation": 0 + }, + { + "dt": 1620512880, + "precipitation": 0 + } + ], + "hourly": [ + { + "dt": 1620507600, + "temp": 284.43, + "feels_like": 283.73, + "pressure": 987, + "humidity": 81, + "dew_point": 281.29, + "uvi": 0, + "clouds": 65, + "visibility": 10000, + "wind_speed": 13.78, + "wind_deg": 190, + "wind_gust": 17.82, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10n" + } + ], + "pop": 0.68, + "rain": { + "1h": 0.23 + } + }, + { + "dt": 1620511200, + "temp": 284.4, + "feels_like": 283.7, + "pressure": 987, + "humidity": 81, + "dew_point": 281.26, + "uvi": 0, + "clouds": 66, + "visibility": 10000, + "wind_speed": 14.25, + "wind_deg": 192, + "wind_gust": 18.2, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04n" + } + ], + "pop": 0.85 + }, + { + "dt": 1620514800, + "temp": 284.35, + "feels_like": 283.67, + "pressure": 987, + "humidity": 82, + "dew_point": 281.39, + "uvi": 0, + "clouds": 68, + "visibility": 10000, + "wind_speed": 14.18, + "wind_deg": 192, + "wind_gust": 18.18, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04n" + } + ], + "pop": 0.73 + }, + { + "dt": 1620518400, + "temp": 284.25, + "feels_like": 283.58, + "pressure": 986, + "humidity": 83, + "dew_point": 281.48, + "uvi": 0, + "clouds": 70, + "visibility": 10000, + "wind_speed": 14.23, + "wind_deg": 190, + "wind_gust": 18.13, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10n" + } + ], + "pop": 0.94, + "rain": { + "1h": 0.13 + } + }, + { + "dt": 1620522000, + "temp": 284.11, + "feels_like": 283.45, + "pressure": 985, + "humidity": 84, + "dew_point": 281.52, + "uvi": 0, + "clouds": 27, + "visibility": 10000, + "wind_speed": 14.14, + "wind_deg": 192, + "wind_gust": 18.12, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10n" + } + ], + "pop": 0.68, + "rain": { + "1h": 0.17 + } + }, + { + "dt": 1620525600, + "temp": 284, + "feels_like": 283.33, + "pressure": 984, + "humidity": 84, + "dew_point": 281.54, + "uvi": 0, + "clouds": 23, + "visibility": 10000, + "wind_speed": 14.29, + "wind_deg": 192, + "wind_gust": 18.23, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10n" + } + ], + "pop": 0.57, + "rain": { + "1h": 0.11 + } + }, + { + "dt": 1620529200, + "temp": 284, + "feels_like": 283.33, + "pressure": 983, + "humidity": 84, + "dew_point": 281.53, + "uvi": 0, + "clouds": 31, + "visibility": 10000, + "wind_speed": 14.22, + "wind_deg": 191, + "wind_gust": 18.1, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03n" + } + ], + "pop": 0.55 + }, + { + "dt": 1620532800, + "temp": 283.99, + "feels_like": 283.32, + "pressure": 983, + "humidity": 84, + "dew_point": 281.63, + "uvi": 0, + "clouds": 32, + "visibility": 10000, + "wind_speed": 14.66, + "wind_deg": 189, + "wind_gust": 18.59, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10n" + } + ], + "pop": 0.59, + "rain": { + "1h": 0.11 + } + }, + { + "dt": 1620536400, + "temp": 283.94, + "feels_like": 283.32, + "pressure": 982, + "humidity": 86, + "dew_point": 281.83, + "uvi": 0, + "clouds": 43, + "visibility": 10000, + "wind_speed": 15.2, + "wind_deg": 189, + "wind_gust": 19.13, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.63, + "rain": { + "1h": 0.2 + } + }, + { + "dt": 1620540000, + "temp": 284.26, + "feels_like": 283.57, + "pressure": 982, + "humidity": 82, + "dew_point": 281.45, + "uvi": 0.11, + "clouds": 52, + "visibility": 10000, + "wind_speed": 15.65, + "wind_deg": 195, + "wind_gust": 18.8, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.59, + "rain": { + "1h": 0.17 + } + }, + { + "dt": 1620543600, + "temp": 284.17, + "feels_like": 283.6, + "pressure": 982, + "humidity": 87, + "dew_point": 282.15, + "uvi": 0.5, + "clouds": 93, + "visibility": 10000, + "wind_speed": 14.76, + "wind_deg": 193, + "wind_gust": 18.18, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.55 + }, + { + "dt": 1620547200, + "temp": 284.18, + "feels_like": 283.61, + "pressure": 982, + "humidity": 87, + "dew_point": 282.13, + "uvi": 1.18, + "clouds": 62, + "visibility": 10000, + "wind_speed": 14.41, + "wind_deg": 196, + "wind_gust": 17.95, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.59, + "rain": { + "1h": 0.16 + } + }, + { + "dt": 1620550800, + "temp": 284.23, + "feels_like": 283.64, + "pressure": 982, + "humidity": 86, + "dew_point": 282.03, + "uvi": 2.2, + "clouds": 49, + "visibility": 10000, + "wind_speed": 14.22, + "wind_deg": 200, + "wind_gust": 18.08, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.59, + "rain": { + "1h": 0.14 + } + }, + { + "dt": 1620554400, + "temp": 284.21, + "feels_like": 283.62, + "pressure": 983, + "humidity": 86, + "dew_point": 282.03, + "uvi": 3.55, + "clouds": 46, + "visibility": 10000, + "wind_speed": 14.77, + "wind_deg": 202, + "wind_gust": 18.58, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.59, + "rain": { + "1h": 0.2 + } + }, + { + "dt": 1620558000, + "temp": 284.23, + "feels_like": 283.64, + "pressure": 983, + "humidity": 86, + "dew_point": 282.03, + "uvi": 4.7, + "clouds": 44, + "visibility": 10000, + "wind_speed": 14.73, + "wind_deg": 204, + "wind_gust": 18.73, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.59, + "rain": { + "1h": 0.11 + } + }, + { + "dt": 1620561600, + "temp": 284.23, + "feels_like": 283.64, + "pressure": 983, + "humidity": 86, + "dew_point": 282.13, + "uvi": 5.43, + "clouds": 42, + "visibility": 10000, + "wind_speed": 14.36, + "wind_deg": 206, + "wind_gust": 18.14, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03d" + } + ], + "pop": 0.59 + }, + { + "dt": 1620565200, + "temp": 284.28, + "feels_like": 283.72, + "pressure": 983, + "humidity": 87, + "dew_point": 282.3, + "uvi": 5.26, + "clouds": 52, + "visibility": 10000, + "wind_speed": 13.22, + "wind_deg": 210, + "wind_gust": 16.49, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0.43 + }, + { + "dt": 1620568800, + "temp": 284.26, + "feels_like": 283.7, + "pressure": 983, + "humidity": 87, + "dew_point": 282.33, + "uvi": 4.75, + "clouds": 49, + "visibility": 10000, + "wind_speed": 11.84, + "wind_deg": 213, + "wind_gust": 14.97, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.43, + "rain": { + "1h": 0.16 + } + }, + { + "dt": 1620572400, + "temp": 284.3, + "feels_like": 283.74, + "pressure": 984, + "humidity": 87, + "dew_point": 282.35, + "uvi": 3.76, + "clouds": 37, + "visibility": 10000, + "wind_speed": 11.1, + "wind_deg": 215, + "wind_gust": 14.18, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03d" + } + ], + "pop": 0.43 + }, + { + "dt": 1620576000, + "temp": 284.27, + "feels_like": 283.76, + "pressure": 984, + "humidity": 89, + "dew_point": 282.53, + "uvi": 2.81, + "clouds": 32, + "visibility": 10000, + "wind_speed": 10.08, + "wind_deg": 217, + "wind_gust": 12.75, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03d" + } + ], + "pop": 0.4 + }, + { + "dt": 1620579600, + "temp": 284.19, + "feels_like": 283.67, + "pressure": 984, + "humidity": 89, + "dew_point": 282.64, + "uvi": 1.6, + "clouds": 28, + "visibility": 10000, + "wind_speed": 9.24, + "wind_deg": 219, + "wind_gust": 11.54, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.4, + "rain": { + "1h": 0.11 + } + }, + { + "dt": 1620583200, + "temp": 284.16, + "feels_like": 283.64, + "pressure": 985, + "humidity": 89, + "dew_point": 282.49, + "uvi": 0.73, + "clouds": 26, + "visibility": 10000, + "wind_speed": 8.05, + "wind_deg": 230, + "wind_gust": 9.99, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.4, + "rain": { + "1h": 0.23 + } + }, + { + "dt": 1620586800, + "temp": 284.1, + "feels_like": 283.55, + "pressure": 985, + "humidity": 88, + "dew_point": 282.33, + "uvi": 0.24, + "clouds": 25, + "visibility": 10000, + "wind_speed": 7.81, + "wind_deg": 240, + "wind_gust": 10.13, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03d" + } + ], + "pop": 0.55 + }, + { + "dt": 1620590400, + "temp": 283.93, + "feels_like": 283.36, + "pressure": 985, + "humidity": 88, + "dew_point": 282.23, + "uvi": 0, + "clouds": 24, + "visibility": 10000, + "wind_speed": 8.03, + "wind_deg": 248, + "wind_gust": 10.05, + "weather": [ + { + "id": 801, + "main": "Clouds", + "description": "few clouds", + "icon": "02d" + } + ], + "pop": 0.41 + }, + { + "dt": 1620594000, + "temp": 283.8, + "feels_like": 283.24, + "pressure": 986, + "humidity": 89, + "dew_point": 282.1, + "uvi": 0, + "clouds": 39, + "visibility": 10000, + "wind_speed": 8.15, + "wind_deg": 249, + "wind_gust": 10.17, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03n" + } + ], + "pop": 0.36 + }, + { + "dt": 1620597600, + "temp": 283.68, + "feels_like": 283.14, + "pressure": 986, + "humidity": 90, + "dew_point": 282.1, + "uvi": 0, + "clouds": 55, + "visibility": 10000, + "wind_speed": 8.09, + "wind_deg": 251, + "wind_gust": 9.95, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04n" + } + ], + "pop": 0.32 + }, + { + "dt": 1620601200, + "temp": 283.54, + "feels_like": 282.96, + "pressure": 986, + "humidity": 89, + "dew_point": 281.98, + "uvi": 0, + "clouds": 64, + "visibility": 10000, + "wind_speed": 7.73, + "wind_deg": 253, + "wind_gust": 9.53, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10n" + } + ], + "pop": 0.29, + "rain": { + "1h": 0.11 + } + }, + { + "dt": 1620604800, + "temp": 283.61, + "feels_like": 283.04, + "pressure": 986, + "humidity": 89, + "dew_point": 281.95, + "uvi": 0, + "clouds": 69, + "visibility": 10000, + "wind_speed": 7.38, + "wind_deg": 254, + "wind_gust": 9.35, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04n" + } + ], + "pop": 0.32 + }, + { + "dt": 1620608400, + "temp": 283.59, + "feels_like": 282.99, + "pressure": 986, + "humidity": 88, + "dew_point": 281.89, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 7.44, + "wind_deg": 264, + "wind_gust": 8.93, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10n" + } + ], + "pop": 0.64, + "rain": { + "1h": 0.19 + } + }, + { + "dt": 1620612000, + "temp": 283.43, + "feels_like": 282.81, + "pressure": 987, + "humidity": 88, + "dew_point": 281.73, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 8.66, + "wind_deg": 281, + "wind_gust": 10.05, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10n" + } + ], + "pop": 0.93, + "rain": { + "1h": 0.58 + } + }, + { + "dt": 1620615600, + "temp": 283.49, + "feels_like": 282.85, + "pressure": 987, + "humidity": 87, + "dew_point": 281.5, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 9.63, + "wind_deg": 277, + "wind_gust": 11.1, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10n" + } + ], + "pop": 0.93, + "rain": { + "1h": 0.44 + } + }, + { + "dt": 1620619200, + "temp": 283.24, + "feels_like": 282.58, + "pressure": 988, + "humidity": 87, + "dew_point": 281.38, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 10.31, + "wind_deg": 279, + "wind_gust": 11.8, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10n" + } + ], + "pop": 0.93, + "rain": { + "1h": 0.2 + } + }, + { + "dt": 1620622800, + "temp": 283.06, + "feels_like": 279.12, + "pressure": 988, + "humidity": 85, + "dew_point": 280.72, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 10.61, + "wind_deg": 281, + "wind_gust": 12.11, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.93, + "rain": { + "1h": 0.38 + } + }, + { + "dt": 1620626400, + "temp": 283.03, + "feels_like": 279, + "pressure": 989, + "humidity": 81, + "dew_point": 280.03, + "uvi": 0.1, + "clouds": 100, + "visibility": 10000, + "wind_speed": 11.01, + "wind_deg": 275, + "wind_gust": 12.17, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.93, + "rain": { + "1h": 0.17 + } + }, + { + "dt": 1620630000, + "temp": 282.97, + "feels_like": 278.86, + "pressure": 990, + "humidity": 80, + "dew_point": 279.71, + "uvi": 0.31, + "clouds": 100, + "visibility": 10000, + "wind_speed": 11.32, + "wind_deg": 275, + "wind_gust": 12.54, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.88, + "rain": { + "1h": 0.11 + } + }, + { + "dt": 1620633600, + "temp": 282.82, + "feels_like": 278.44, + "pressure": 991, + "humidity": 79, + "dew_point": 279.39, + "uvi": 0.73, + "clouds": 98, + "visibility": 10000, + "wind_speed": 12.49, + "wind_deg": 275, + "wind_gust": 13.8, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 1, + "rain": { + "1h": 0.25 + } + }, + { + "dt": 1620637200, + "temp": 282.84, + "feels_like": 278.33, + "pressure": 992, + "humidity": 75, + "dew_point": 278.74, + "uvi": 1.35, + "clouds": 99, + "visibility": 10000, + "wind_speed": 13.27, + "wind_deg": 272, + "wind_gust": 14.52, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 1, + "rain": { + "1h": 0.23 + } + }, + { + "dt": 1620640800, + "temp": 282.64, + "feels_like": 278.1, + "pressure": 993, + "humidity": 73, + "dew_point": 278.15, + "uvi": 2.73, + "clouds": 99, + "visibility": 10000, + "wind_speed": 13.04, + "wind_deg": 267, + "wind_gust": 15.01, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.8 + }, + { + "dt": 1620644400, + "temp": 282.69, + "feels_like": 278.16, + "pressure": 993, + "humidity": 72, + "dew_point": 277.93, + "uvi": 3.6, + "clouds": 96, + "visibility": 10000, + "wind_speed": 13.07, + "wind_deg": 255, + "wind_gust": 14.91, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 1, + "rain": { + "1h": 0.22 + } + }, + { + "dt": 1620648000, + "temp": 282.96, + "feels_like": 278.64, + "pressure": 994, + "humidity": 70, + "dew_point": 277.93, + "uvi": 4.16, + "clouds": 94, + "visibility": 10000, + "wind_speed": 12.41, + "wind_deg": 245, + "wind_gust": 14.22, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.79 + }, + { + "dt": 1620651600, + "temp": 283.01, + "feels_like": 278.78, + "pressure": 994, + "humidity": 68, + "dew_point": 277.61, + "uvi": 4.5, + "clouds": 86, + "visibility": 10000, + "wind_speed": 12.05, + "wind_deg": 237, + "wind_gust": 13.78, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.39 + }, + { + "dt": 1620655200, + "temp": 282.93, + "feels_like": 278.56, + "pressure": 995, + "humidity": 68, + "dew_point": 277.5, + "uvi": 4.07, + "clouds": 85, + "visibility": 10000, + "wind_speed": 12.66, + "wind_deg": 234, + "wind_gust": 13.9, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.4 + }, + { + "dt": 1620658800, + "temp": 282.89, + "feels_like": 278.56, + "pressure": 995, + "humidity": 68, + "dew_point": 277.38, + "uvi": 3.23, + "clouds": 61, + "visibility": 10000, + "wind_speed": 12.36, + "wind_deg": 228, + "wind_gust": 13.73, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.4, + "rain": { + "1h": 0.11 + } + }, + { + "dt": 1620662400, + "temp": 282.73, + "feels_like": 278.33, + "pressure": 995, + "humidity": 69, + "dew_point": 277.56, + "uvi": 2.42, + "clouds": 58, + "visibility": 10000, + "wind_speed": 12.41, + "wind_deg": 221, + "wind_gust": 13.37, + "weather": [ + { + "id": 803, + "main": "Clouds", + "description": "broken clouds", + "icon": "04d" + } + ], + "pop": 0.4 + }, + { + "dt": 1620666000, + "temp": 282.76, + "feels_like": 278.44, + "pressure": 995, + "humidity": 69, + "dew_point": 277.59, + "uvi": 1.4, + "clouds": 48, + "visibility": 10000, + "wind_speed": 12.06, + "wind_deg": 217, + "wind_gust": 13.38, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03d" + } + ], + "pop": 0.4 + }, + { + "dt": 1620669600, + "temp": 282.85, + "feels_like": 278.64, + "pressure": 996, + "humidity": 70, + "dew_point": 277.73, + "uvi": 0.65, + "clouds": 42, + "visibility": 10000, + "wind_speed": 11.64, + "wind_deg": 215, + "wind_gust": 13.1, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.44, + "rain": { + "1h": 0.17 + } + }, + { + "dt": 1620673200, + "temp": 282.87, + "feels_like": 278.71, + "pressure": 996, + "humidity": 70, + "dew_point": 277.81, + "uvi": 0.22, + "clouds": 32, + "visibility": 10000, + "wind_speed": 11.38, + "wind_deg": 212, + "wind_gust": 12.78, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.58, + "rain": { + "1h": 0.17 + } + }, + { + "dt": 1620676800, + "temp": 282.97, + "feels_like": 278.93, + "pressure": 996, + "humidity": 69, + "dew_point": 277.71, + "uvi": 0, + "clouds": 33, + "visibility": 10000, + "wind_speed": 10.92, + "wind_deg": 204, + "wind_gust": 12.2, + "weather": [ + { + "id": 802, + "main": "Clouds", + "description": "scattered clouds", + "icon": "03d" + } + ], + "pop": 0.47 + } + ], + "daily": [ + { + "dt": 1620475200, + "sunrise": 1620449957, + "sunset": 1620504821, + "moonrise": 1620447540, + "moonset": 1620492060, + "moon_phase": 0.9, + "temp": { + "day": 284.62, + "min": 283.83, + "max": 285.33, + "night": 284.4, + "eve": 284.54, + "morn": 284.62 + }, + "feels_like": { + "day": 284.07, + "night": 284.25, + "eve": 283.88, + "morn": 284.25 + }, + "pressure": 993, + "humidity": 86, + "dew_point": 282.44, + "wind_speed": 14.63, + "wind_deg": 120, + "wind_gust": 19.66, + "weather": [ + { + "id": 501, + "main": "Rain", + "description": "moderate rain", + "icon": "10d" + } + ], + "clouds": 25, + "pop": 1, + "rain": 11.17, + "uvi": 6.25 + }, + { + "dt": 1620561600, + "sunrise": 1620536258, + "sunset": 1620591317, + "moonrise": 1620534780, + "moonset": 1620582540, + "moon_phase": 0.93, + "temp": { + "day": 284.23, + "min": 283.68, + "max": 284.35, + "night": 283.68, + "eve": 284.16, + "morn": 284.26 + }, + "feels_like": { + "day": 283.64, + "night": 283.57, + "eve": 283.64, + "morn": 283.57 + }, + "pressure": 983, + "humidity": 86, + "dew_point": 282.13, + "wind_speed": 15.65, + "wind_deg": 195, + "wind_gust": 19.13, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "clouds": 42, + "pop": 0.94, + "rain": 2, + "uvi": 5.43 + }, + { + "dt": 1620648000, + "sunrise": 1620622560, + "sunset": 1620677812, + "moonrise": 1620622020, + "moonset": 1620673080, + "moon_phase": 0.96, + "temp": { + "day": 282.96, + "min": 282.64, + "max": 283.61, + "night": 282.92, + "eve": 282.85, + "morn": 283.03 + }, + "feels_like": { + "day": 278.64, + "night": 279, + "eve": 278.64, + "morn": 279 + }, + "pressure": 994, + "humidity": 70, + "dew_point": 277.93, + "wind_speed": 13.27, + "wind_deg": 272, + "wind_gust": 15.01, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "clouds": 94, + "pop": 1, + "rain": 3.53, + "uvi": 4.5 + }, + { + "dt": 1620734400, + "sunrise": 1620708863, + "sunset": 1620764306, + "moonrise": 1620709320, + "moonset": 1620763620, + "moon_phase": 0, + "temp": { + "day": 282.77, + "min": 281.89, + "max": 284.08, + "night": 284.08, + "eve": 283.35, + "morn": 282.57 + }, + "feels_like": { + "day": 278.82, + "night": 278.28, + "eve": 282.51, + "morn": 278.28 + }, + "pressure": 995, + "humidity": 87, + "dew_point": 280.87, + "wind_speed": 12.31, + "wind_deg": 132, + "wind_gust": 13.13, + "weather": [ + { + "id": 501, + "main": "Rain", + "description": "moderate rain", + "icon": "10d" + } + ], + "clouds": 100, + "pop": 1, + "rain": 4.63, + "uvi": 1.66 + }, + { + "dt": 1620820800, + "sunrise": 1620795169, + "sunset": 1620850800, + "moonrise": 1620796860, + "moonset": 1620854160, + "moon_phase": 0.02, + "temp": { + "day": 283.83, + "min": 282.8, + "max": 284.28, + "night": 284.01, + "eve": 284.28, + "morn": 282.8 + }, + "feels_like": { + "day": 282.89, + "night": 281.15, + "eve": 283.51, + "morn": 281.15 + }, + "pressure": 1007, + "humidity": 74, + "dew_point": 279.45, + "wind_speed": 6.7, + "wind_deg": 297, + "wind_gust": 7.73, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "clouds": 37, + "pop": 0.2, + "rain": 0.2, + "uvi": 5.08 + }, + { + "dt": 1620907200, + "sunrise": 1620881476, + "sunset": 1620937293, + "moonrise": 1620884640, + "moonset": 1620944640, + "moon_phase": 0.05, + "temp": { + "day": 283.55, + "min": 283.18, + "max": 283.55, + "night": 283.34, + "eve": 283.44, + "morn": 283.18 + }, + "feels_like": { + "day": 282.87, + "night": 282.33, + "eve": 282.82, + "morn": 282.33 + }, + "pressure": 1011, + "humidity": 85, + "dew_point": 281.2, + "wind_speed": 11.94, + "wind_deg": 316, + "wind_gust": 12.37, + "weather": [ + { + "id": 800, + "main": "Clear", + "description": "clear sky", + "icon": "01d" + } + ], + "clouds": 4, + "pop": 0.26, + "uvi": 4.96 + }, + { + "dt": 1620993600, + "sunrise": 1620967786, + "sunset": 1621023785, + "moonrise": 1620972900, + "moonset": 0, + "moon_phase": 0.08, + "temp": { + "day": 284.2, + "min": 283.18, + "max": 285.07, + "night": 284.52, + "eve": 284.9, + "morn": 283.18 + }, + "feels_like": { + "day": 283.61, + "night": 282.48, + "eve": 284.3, + "morn": 282.48 + }, + "pressure": 1013, + "humidity": 86, + "dew_point": 282.02, + "wind_speed": 8.24, + "wind_deg": 336, + "wind_gust": 9.69, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "clouds": 52, + "pop": 0.47, + "rain": 1.03, + "uvi": 5 + }, + { + "dt": 1621080000, + "sunrise": 1621054097, + "sunset": 1621110276, + "moonrise": 1621061640, + "moonset": 1621034760, + "moon_phase": 0.11, + "temp": { + "day": 284.13, + "min": 283.56, + "max": 284.13, + "night": 283.72, + "eve": 284.06, + "morn": 283.68 + }, + "feels_like": { + "day": 283.56, + "night": 282.98, + "eve": 283.53, + "morn": 282.98 + }, + "pressure": 1017, + "humidity": 87, + "dew_point": 282.23, + "wind_speed": 11.6, + "wind_deg": 149, + "wind_gust": 15.48, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "clouds": 100, + "pop": 0.85, + "rain": 0.84, + "uvi": 5 + } + ] +} diff --git a/bundles/org.openhab.binding.ems/weather2.json b/bundles/org.openhab.binding.ems/weather2.json new file mode 100644 index 0000000000000..4411e829daed8 --- /dev/null +++ b/bundles/org.openhab.binding.ems/weather2.json @@ -0,0 +1,1801 @@ +{ + "lat": 50.55, + "lon": 8.43, + "timezone": "Europe/Berlin", + "timezone_offset": 7200, + "current": { + "dt": 1620604620, + "sunrise": 1620618337, + "sunset": 1620673188, + "temp": 288.71, + "feels_like": 288.04, + "pressure": 1004, + "humidity": 66, + "dew_point": 282.39, + "uvi": 0, + "clouds": 96, + "visibility": 10000, + "wind_speed": 0.89, + "wind_deg": 138, + "wind_gust": 1.34, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ] + }, + "minutely": [ + { + "dt": 1620604620, + "precipitation": 0 + }, + { + "dt": 1620604680, + "precipitation": 0 + }, + { + "dt": 1620604740, + "precipitation": 0 + }, + { + "dt": 1620604800, + "precipitation": 0 + }, + { + "dt": 1620604860, + "precipitation": 0 + }, + { + "dt": 1620604920, + "precipitation": 0 + }, + { + "dt": 1620604980, + "precipitation": 0 + }, + { + "dt": 1620605040, + "precipitation": 0 + }, + { + "dt": 1620605100, + "precipitation": 0 + }, + { + "dt": 1620605160, + "precipitation": 0 + }, + { + "dt": 1620605220, + "precipitation": 0 + }, + { + "dt": 1620605280, + "precipitation": 0 + }, + { + "dt": 1620605340, + "precipitation": 0 + }, + { + "dt": 1620605400, + "precipitation": 0 + }, + { + "dt": 1620605460, + "precipitation": 0 + }, + { + "dt": 1620605520, + "precipitation": 0 + }, + { + "dt": 1620605580, + "precipitation": 0 + }, + { + "dt": 1620605640, + "precipitation": 0 + }, + { + "dt": 1620605700, + "precipitation": 0 + }, + { + "dt": 1620605760, + "precipitation": 0 + }, + { + "dt": 1620605820, + "precipitation": 0 + }, + { + "dt": 1620605880, + "precipitation": 0 + }, + { + "dt": 1620605940, + "precipitation": 0 + }, + { + "dt": 1620606000, + "precipitation": 0 + }, + { + "dt": 1620606060, + "precipitation": 0 + }, + { + "dt": 1620606120, + "precipitation": 0 + }, + { + "dt": 1620606180, + "precipitation": 0 + }, + { + "dt": 1620606240, + "precipitation": 0 + }, + { + "dt": 1620606300, + "precipitation": 0 + }, + { + "dt": 1620606360, + "precipitation": 0 + }, + { + "dt": 1620606420, + "precipitation": 0 + }, + { + "dt": 1620606480, + "precipitation": 0 + }, + { + "dt": 1620606540, + "precipitation": 0 + }, + { + "dt": 1620606600, + "precipitation": 0 + }, + { + "dt": 1620606660, + "precipitation": 0 + }, + { + "dt": 1620606720, + "precipitation": 0 + }, + { + "dt": 1620606780, + "precipitation": 0 + }, + { + "dt": 1620606840, + "precipitation": 0 + }, + { + "dt": 1620606900, + "precipitation": 0 + }, + { + "dt": 1620606960, + "precipitation": 0 + }, + { + "dt": 1620607020, + "precipitation": 0 + }, + { + "dt": 1620607080, + "precipitation": 0 + }, + { + "dt": 1620607140, + "precipitation": 0.107 + }, + { + "dt": 1620607200, + "precipitation": 0.115 + }, + { + "dt": 1620607260, + "precipitation": 0.1762 + }, + { + "dt": 1620607320, + "precipitation": 0.2374 + }, + { + "dt": 1620607380, + "precipitation": 0.2986 + }, + { + "dt": 1620607440, + "precipitation": 0.3598 + }, + { + "dt": 1620607500, + "precipitation": 0.421 + }, + { + "dt": 1620607560, + "precipitation": 0.421 + }, + { + "dt": 1620607620, + "precipitation": 0.421 + }, + { + "dt": 1620607680, + "precipitation": 0.421 + }, + { + "dt": 1620607740, + "precipitation": 0.421 + }, + { + "dt": 1620607800, + "precipitation": 0.421 + }, + { + "dt": 1620607860, + "precipitation": 0.3914 + }, + { + "dt": 1620607920, + "precipitation": 0.3618 + }, + { + "dt": 1620607980, + "precipitation": 0.3322 + }, + { + "dt": 1620608040, + "precipitation": 0.3026 + }, + { + "dt": 1620608100, + "precipitation": 0.273 + }, + { + "dt": 1620608160, + "precipitation": 0.2816 + }, + { + "dt": 1620608220, + "precipitation": 0.2902 + } + ], + "hourly": [ + { + "dt": 1620601200, + "temp": 288.64, + "feels_like": 288.1, + "pressure": 1004, + "humidity": 71, + "dew_point": 283.41, + "uvi": 0, + "clouds": 96, + "visibility": 10000, + "wind_speed": 1.09, + "wind_deg": 174, + "wind_gust": 1.43, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0.68 + }, + { + "dt": 1620604800, + "temp": 288.71, + "feels_like": 288.04, + "pressure": 1004, + "humidity": 66, + "dew_point": 282.39, + "uvi": 0, + "clouds": 96, + "visibility": 10000, + "wind_speed": 1.08, + "wind_deg": 197, + "wind_gust": 1.43, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0.65 + }, + { + "dt": 1620608400, + "temp": 288.49, + "feels_like": 287.93, + "pressure": 1004, + "humidity": 71, + "dew_point": 283.27, + "uvi": 0, + "clouds": 97, + "visibility": 10000, + "wind_speed": 0.81, + "wind_deg": 149, + "wind_gust": 0.96, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10n" + } + ], + "pop": 0.22, + "rain": { + "1h": 0.32 + } + }, + { + "dt": 1620612000, + "temp": 288.13, + "feels_like": 287.67, + "pressure": 1004, + "humidity": 76, + "dew_point": 283.94, + "uvi": 0, + "clouds": 98, + "visibility": 10000, + "wind_speed": 1.45, + "wind_deg": 133, + "wind_gust": 1.4, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10n" + } + ], + "pop": 0.2, + "rain": { + "1h": 0.75 + } + }, + { + "dt": 1620615600, + "temp": 287.63, + "feels_like": 287.27, + "pressure": 1004, + "humidity": 82, + "dew_point": 284.6, + "uvi": 0, + "clouds": 98, + "visibility": 10000, + "wind_speed": 1.05, + "wind_deg": 107, + "wind_gust": 1.17, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1620619200, + "temp": 286.55, + "feels_like": 286.24, + "pressure": 1005, + "humidity": 88, + "dew_point": 284.61, + "uvi": 0, + "clouds": 96, + "visibility": 10000, + "wind_speed": 1.25, + "wind_deg": 208, + "wind_gust": 1.28, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.01 + }, + { + "dt": 1620622800, + "temp": 287.06, + "feels_like": 286.96, + "pressure": 1007, + "humidity": 94, + "dew_point": 285.55, + "uvi": 0.15, + "clouds": 97, + "visibility": 10000, + "wind_speed": 1.8, + "wind_deg": 206, + "wind_gust": 2.68, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.02 + }, + { + "dt": 1620626400, + "temp": 288.93, + "feels_like": 288.94, + "pressure": 1007, + "humidity": 91, + "dew_point": 286.94, + "uvi": 0.49, + "clouds": 97, + "visibility": 10000, + "wind_speed": 0.87, + "wind_deg": 160, + "wind_gust": 3.87, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.33, + "rain": { + "1h": 0.2 + } + }, + { + "dt": 1620630000, + "temp": 288.38, + "feels_like": 288.44, + "pressure": 1007, + "humidity": 95, + "dew_point": 286.95, + "uvi": 0.19, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.81, + "wind_deg": 261, + "wind_gust": 4.32, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.47, + "rain": { + "1h": 0.17 + } + }, + { + "dt": 1620633600, + "temp": 288.58, + "feels_like": 288.55, + "pressure": 1008, + "humidity": 91, + "dew_point": 286.47, + "uvi": 0.34, + "clouds": 100, + "visibility": 10000, + "wind_speed": 3.23, + "wind_deg": 249, + "wind_gust": 8.89, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.54 + }, + { + "dt": 1620637200, + "temp": 287.51, + "feels_like": 287.46, + "pressure": 1008, + "humidity": 94, + "dew_point": 285.99, + "uvi": 0.51, + "clouds": 100, + "visibility": 10000, + "wind_speed": 2.19, + "wind_deg": 206, + "wind_gust": 11.01, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.58, + "rain": { + "1h": 0.19 + } + }, + { + "dt": 1620640800, + "temp": 288.19, + "feels_like": 288.1, + "pressure": 1009, + "humidity": 90, + "dew_point": 286.09, + "uvi": 4.45, + "clouds": 100, + "visibility": 10000, + "wind_speed": 3.41, + "wind_deg": 211, + "wind_gust": 10.74, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.54 + }, + { + "dt": 1620644400, + "temp": 288.07, + "feels_like": 287.99, + "pressure": 1009, + "humidity": 91, + "dew_point": 286.07, + "uvi": 5.03, + "clouds": 100, + "visibility": 10000, + "wind_speed": 3.19, + "wind_deg": 227, + "wind_gust": 9.3, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.46 + }, + { + "dt": 1620648000, + "temp": 289.55, + "feels_like": 289.36, + "pressure": 1009, + "humidity": 81, + "dew_point": 285.73, + "uvi": 5.01, + "clouds": 100, + "visibility": 10000, + "wind_speed": 3.24, + "wind_deg": 227, + "wind_gust": 7.62, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.42 + }, + { + "dt": 1620651600, + "temp": 289.55, + "feels_like": 289.33, + "pressure": 1009, + "humidity": 80, + "dew_point": 285.45, + "uvi": 2.14, + "clouds": 100, + "visibility": 10000, + "wind_speed": 2.71, + "wind_deg": 215, + "wind_gust": 5.32, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.46, + "rain": { + "1h": 0.26 + } + }, + { + "dt": 1620655200, + "temp": 290, + "feels_like": 289.67, + "pressure": 1010, + "humidity": 74, + "dew_point": 284.77, + "uvi": 1.64, + "clouds": 100, + "visibility": 10000, + "wind_speed": 3.79, + "wind_deg": 218, + "wind_gust": 6.77, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.34 + }, + { + "dt": 1620658800, + "temp": 289.53, + "feels_like": 289.16, + "pressure": 1010, + "humidity": 74, + "dew_point": 284.36, + "uvi": 1.08, + "clouds": 100, + "visibility": 10000, + "wind_speed": 2.8, + "wind_deg": 204, + "wind_gust": 4.23, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.34 + }, + { + "dt": 1620662400, + "temp": 287.92, + "feels_like": 287.72, + "pressure": 1010, + "humidity": 87, + "dew_point": 285.27, + "uvi": 0.6, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.97, + "wind_deg": 200, + "wind_gust": 3.95, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.5, + "rain": { + "1h": 0.35 + } + }, + { + "dt": 1620666000, + "temp": 286.47, + "feels_like": 286.34, + "pressure": 1010, + "humidity": 95, + "dew_point": 285.09, + "uvi": 0.25, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.21, + "wind_deg": 199, + "wind_gust": 2.26, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.59, + "rain": { + "1h": 0.58 + } + }, + { + "dt": 1620669600, + "temp": 285.83, + "feels_like": 285.66, + "pressure": 1011, + "humidity": 96, + "dew_point": 284.67, + "uvi": 0.07, + "clouds": 100, + "visibility": 7020, + "wind_speed": 1.67, + "wind_deg": 183, + "wind_gust": 2.8, + "weather": [ + { + "id": 501, + "main": "Rain", + "description": "moderate rain", + "icon": "10d" + } + ], + "pop": 0.73, + "rain": { + "1h": 1.07 + } + }, + { + "dt": 1620673200, + "temp": 285.34, + "feels_like": 285.15, + "pressure": 1010, + "humidity": 97, + "dew_point": 284.28, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 0.43, + "wind_deg": 304, + "wind_gust": 0.9, + "weather": [ + { + "id": 501, + "main": "Rain", + "description": "moderate rain", + "icon": "10n" + } + ], + "pop": 1, + "rain": { + "1h": 2.03 + } + }, + { + "dt": 1620676800, + "temp": 285.12, + "feels_like": 284.93, + "pressure": 1011, + "humidity": 98, + "dew_point": 284.16, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 0.89, + "wind_deg": 299, + "wind_gust": 1.16, + "weather": [ + { + "id": 501, + "main": "Rain", + "description": "moderate rain", + "icon": "10n" + } + ], + "pop": 1, + "rain": { + "1h": 1.09 + } + }, + { + "dt": 1620680400, + "temp": 285.05, + "feels_like": 284.85, + "pressure": 1010, + "humidity": 98, + "dew_point": 284.18, + "uvi": 0, + "clouds": 100, + "visibility": 9686, + "wind_speed": 0.91, + "wind_deg": 165, + "wind_gust": 0.97, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10n" + } + ], + "pop": 1, + "rain": { + "1h": 0.81 + } + }, + { + "dt": 1620684000, + "temp": 284.9, + "feels_like": 284.72, + "pressure": 1010, + "humidity": 99, + "dew_point": 284.1, + "uvi": 0, + "clouds": 100, + "visibility": 8743, + "wind_speed": 0.14, + "wind_deg": 152, + "wind_gust": 1.22, + "weather": [ + { + "id": 501, + "main": "Rain", + "description": "moderate rain", + "icon": "10n" + } + ], + "pop": 1, + "rain": { + "1h": 1.12 + } + }, + { + "dt": 1620687600, + "temp": 284.9, + "feels_like": 284.72, + "pressure": 1010, + "humidity": 99, + "dew_point": 284.1, + "uvi": 0, + "clouds": 100, + "visibility": 6532, + "wind_speed": 0.99, + "wind_deg": 226, + "wind_gust": 1.77, + "weather": [ + { + "id": 501, + "main": "Rain", + "description": "moderate rain", + "icon": "10n" + } + ], + "pop": 1, + "rain": { + "1h": 3.25 + } + }, + { + "dt": 1620691200, + "temp": 284.84, + "feels_like": 284.65, + "pressure": 1010, + "humidity": 99, + "dew_point": 284.03, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 0.88, + "wind_deg": 249, + "wind_gust": 1.28, + "weather": [ + { + "id": 501, + "main": "Rain", + "description": "moderate rain", + "icon": "10n" + } + ], + "pop": 1, + "rain": { + "1h": 2.1 + } + }, + { + "dt": 1620694800, + "temp": 284.75, + "feels_like": 284.52, + "pressure": 1009, + "humidity": 98, + "dew_point": 283.92, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.15, + "wind_deg": 277, + "wind_gust": 2.28, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10n" + } + ], + "pop": 0.89, + "rain": { + "1h": 0.5 + } + }, + { + "dt": 1620698400, + "temp": 284.9, + "feels_like": 284.69, + "pressure": 1008, + "humidity": 98, + "dew_point": 284.07, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.28, + "wind_deg": 274, + "wind_gust": 3.79, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10n" + } + ], + "pop": 0.92, + "rain": { + "1h": 0.76 + } + }, + { + "dt": 1620702000, + "temp": 284.98, + "feels_like": 284.78, + "pressure": 1009, + "humidity": 98, + "dew_point": 284.12, + "uvi": 0, + "clouds": 100, + "visibility": 6157, + "wind_speed": 1.95, + "wind_deg": 242, + "wind_gust": 3.74, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10n" + } + ], + "pop": 0.97, + "rain": { + "1h": 0.88 + } + }, + { + "dt": 1620705600, + "temp": 284.95, + "feels_like": 284.74, + "pressure": 1009, + "humidity": 98, + "dew_point": 284, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.2, + "wind_deg": 289, + "wind_gust": 4.04, + "weather": [ + { + "id": 501, + "main": "Rain", + "description": "moderate rain", + "icon": "10d" + } + ], + "pop": 1, + "rain": { + "1h": 1.44 + } + }, + { + "dt": 1620709200, + "temp": 285, + "feels_like": 284.8, + "pressure": 1009, + "humidity": 98, + "dew_point": 284.12, + "uvi": 0.02, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.3, + "wind_deg": 293, + "wind_gust": 3.56, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 1, + "rain": { + "1h": 0.65 + } + }, + { + "dt": 1620712800, + "temp": 285.3, + "feels_like": 285.1, + "pressure": 1008, + "humidity": 97, + "dew_point": 284.22, + "uvi": 0.05, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.63, + "wind_deg": 284, + "wind_gust": 4.51, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 1, + "rain": { + "1h": 0.29 + } + }, + { + "dt": 1620716400, + "temp": 285.89, + "feels_like": 285.67, + "pressure": 1009, + "humidity": 94, + "dew_point": 284.42, + "uvi": 0.19, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.64, + "wind_deg": 277, + "wind_gust": 3.33, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.37, + "rain": { + "1h": 0.14 + } + }, + { + "dt": 1620720000, + "temp": 285.77, + "feels_like": 285.57, + "pressure": 1008, + "humidity": 95, + "dew_point": 284.44, + "uvi": 0.33, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.5, + "wind_deg": 314, + "wind_gust": 3.17, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.35, + "rain": { + "1h": 0.14 + } + }, + { + "dt": 1620723600, + "temp": 286.46, + "feels_like": 286.25, + "pressure": 1008, + "humidity": 92, + "dew_point": 284.63, + "uvi": 0.5, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.87, + "wind_deg": 34, + "wind_gust": 5.4, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.35 + }, + { + "dt": 1620727200, + "temp": 286.3, + "feels_like": 286.07, + "pressure": 1008, + "humidity": 92, + "dew_point": 284.47, + "uvi": 0.8, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.57, + "wind_deg": 41, + "wind_gust": 4.81, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.27 + }, + { + "dt": 1620730800, + "temp": 286.08, + "feels_like": 285.83, + "pressure": 1008, + "humidity": 92, + "dew_point": 284.32, + "uvi": 0.9, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.28, + "wind_deg": 46, + "wind_gust": 3.42, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.4, + "rain": { + "1h": 0.15 + } + }, + { + "dt": 1620734400, + "temp": 285.84, + "feels_like": 285.62, + "pressure": 1009, + "humidity": 94, + "dew_point": 284.32, + "uvi": 0.89, + "clouds": 100, + "visibility": 10000, + "wind_speed": 0.56, + "wind_deg": 66, + "wind_gust": 2.08, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.54, + "rain": { + "1h": 0.27 + } + }, + { + "dt": 1620738000, + "temp": 285.58, + "feels_like": 285.39, + "pressure": 1009, + "humidity": 96, + "dew_point": 284.32, + "uvi": 0.42, + "clouds": 100, + "visibility": 10000, + "wind_speed": 0.66, + "wind_deg": 356, + "wind_gust": 2.17, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.7, + "rain": { + "1h": 0.37 + } + }, + { + "dt": 1620741600, + "temp": 285.49, + "feels_like": 285.29, + "pressure": 1009, + "humidity": 96, + "dew_point": 284.29, + "uvi": 0.32, + "clouds": 100, + "visibility": 10000, + "wind_speed": 0.69, + "wind_deg": 333, + "wind_gust": 1.83, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.72, + "rain": { + "1h": 0.27 + } + }, + { + "dt": 1620745200, + "temp": 285.3, + "feels_like": 285.08, + "pressure": 1008, + "humidity": 96, + "dew_point": 284.09, + "uvi": 0.21, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.44, + "wind_deg": 26, + "wind_gust": 4.43, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.71 + }, + { + "dt": 1620748800, + "temp": 286.61, + "feels_like": 286.28, + "pressure": 1008, + "humidity": 87, + "dew_point": 283.96, + "uvi": 0.49, + "clouds": 100, + "visibility": 10000, + "wind_speed": 0.98, + "wind_deg": 23, + "wind_gust": 2.28, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.69 + }, + { + "dt": 1620752400, + "temp": 286.4, + "feels_like": 286.1, + "pressure": 1008, + "humidity": 89, + "dew_point": 284.06, + "uvi": 0.21, + "clouds": 100, + "visibility": 10000, + "wind_speed": 0.96, + "wind_deg": 44, + "wind_gust": 1.85, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.69 + }, + { + "dt": 1620756000, + "temp": 285.73, + "feels_like": 285.45, + "pressure": 1008, + "humidity": 92, + "dew_point": 283.98, + "uvi": 0.06, + "clouds": 100, + "visibility": 10000, + "wind_speed": 0.73, + "wind_deg": 7, + "wind_gust": 0.88, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.61 + }, + { + "dt": 1620759600, + "temp": 284.5, + "feels_like": 284.17, + "pressure": 1009, + "humidity": 95, + "dew_point": 283.12, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 0.06, + "wind_deg": 265, + "wind_gust": 0.39, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.07 + }, + { + "dt": 1620763200, + "temp": 284.59, + "feels_like": 284.27, + "pressure": 1009, + "humidity": 95, + "dew_point": 283.18, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 0.09, + "wind_deg": 319, + "wind_gust": 0.35, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0.04 + }, + { + "dt": 1620766800, + "temp": 284.38, + "feels_like": 284.04, + "pressure": 1009, + "humidity": 95, + "dew_point": 283.09, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 0.79, + "wind_deg": 279, + "wind_gust": 0.75, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1620770400, + "temp": 284.37, + "feels_like": 284.05, + "pressure": 1008, + "humidity": 96, + "dew_point": 283.17, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.33, + "wind_deg": 277, + "wind_gust": 1.62, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + } + ], + "daily": [ + { + "dt": 1620644400, + "sunrise": 1620618337, + "sunset": 1620673188, + "moonrise": 1620617640, + "moonset": 1620668340, + "moon_phase": 0.96, + "temp": { + "day": 288.07, + "min": 285.05, + "max": 290, + "night": 285.05, + "eve": 286.47, + "morn": 287.06 + }, + "feels_like": { + "day": 287.99, + "night": 286.96, + "eve": 286.34, + "morn": 286.96 + }, + "pressure": 1009, + "humidity": 91, + "dew_point": 286.07, + "wind_speed": 3.79, + "wind_deg": 218, + "wind_gust": 11.01, + "weather": [ + { + "id": 501, + "main": "Rain", + "description": "moderate rain", + "icon": "10d" + } + ], + "clouds": 100, + "pop": 1, + "rain": 8.25, + "uvi": 5.03 + }, + { + "dt": 1620730800, + "sunrise": 1620704645, + "sunset": 1620759679, + "moonrise": 1620705000, + "moonset": 1620758820, + "moon_phase": 0, + "temp": { + "day": 286.08, + "min": 284.38, + "max": 286.61, + "night": 284.38, + "eve": 286.4, + "morn": 285 + }, + "feels_like": { + "day": 285.83, + "night": 284.8, + "eve": 286.1, + "morn": 284.8 + }, + "pressure": 1008, + "humidity": 92, + "dew_point": 284.32, + "wind_speed": 1.95, + "wind_deg": 242, + "wind_gust": 5.4, + "weather": [ + { + "id": 501, + "main": "Rain", + "description": "moderate rain", + "icon": "10d" + } + ], + "clouds": 100, + "pop": 1, + "rain": 12.33, + "uvi": 0.9 + }, + { + "dt": 1620817200, + "sunrise": 1620790954, + "sunset": 1620846169, + "moonrise": 1620792540, + "moonset": 1620849300, + "moon_phase": 0.02, + "temp": { + "day": 284.66, + "min": 283.18, + "max": 285.13, + "night": 283.18, + "eve": 284.2, + "morn": 284.05 + }, + "feels_like": { + "day": 284.32, + "night": 283.73, + "eve": 283.76, + "morn": 283.73 + }, + "pressure": 1009, + "humidity": 94, + "dew_point": 283.13, + "wind_speed": 4.8, + "wind_deg": 291, + "wind_gust": 8.85, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "clouds": 100, + "pop": 0.81, + "rain": 2, + "uvi": 1.82 + }, + { + "dt": 1620903600, + "sunrise": 1620877264, + "sunset": 1620932658, + "moonrise": 1620880380, + "moonset": 1620939720, + "moon_phase": 0.05, + "temp": { + "day": 283.74, + "min": 281.94, + "max": 284.22, + "night": 282.39, + "eve": 282.64, + "morn": 281.94 + }, + "feels_like": { + "day": 283.26, + "night": 280.92, + "eve": 282.64, + "morn": 280.92 + }, + "pressure": 1008, + "humidity": 92, + "dew_point": 281.84, + "wind_speed": 3.28, + "wind_deg": 303, + "wind_gust": 7.5, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "clouds": 100, + "pop": 0.81, + "rain": 3, + "uvi": 1.83 + }, + { + "dt": 1620990000, + "sunrise": 1620963577, + "sunset": 1621019147, + "moonrise": 1620968640, + "moonset": 0, + "moon_phase": 0.08, + "temp": { + "day": 290.79, + "min": 281.49, + "max": 291.23, + "night": 282.39, + "eve": 287.13, + "morn": 283.24 + }, + "feels_like": { + "day": 289.68, + "night": 282.73, + "eve": 286.33, + "morn": 282.73 + }, + "pressure": 1009, + "humidity": 41, + "dew_point": 276.98, + "wind_speed": 2.9, + "wind_deg": 281, + "wind_gust": 3.76, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "clouds": 100, + "pop": 0.37, + "rain": 0.19, + "uvi": 5.25 + }, + { + "dt": 1621076400, + "sunrise": 1621049892, + "sunset": 1621105635, + "moonrise": 1621057380, + "moonset": 1621029840, + "moon_phase": 0.11, + "temp": { + "day": 290.66, + "min": 279.77, + "max": 290.66, + "night": 282.33, + "eve": 287.03, + "morn": 283.92 + }, + "feels_like": { + "day": 289.88, + "night": 282.98, + "eve": 286.48, + "morn": 282.98 + }, + "pressure": 1013, + "humidity": 54, + "dew_point": 280.69, + "wind_speed": 2.06, + "wind_deg": 10, + "wind_gust": 5, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "clouds": 99, + "pop": 0.39, + "rain": 0.59, + "uvi": 6 + }, + { + "dt": 1621162800, + "sunrise": 1621136208, + "sunset": 1621192121, + "moonrise": 1621146780, + "moonset": 1621119600, + "moon_phase": 0.14, + "temp": { + "day": 282.67, + "min": 280.69, + "max": 287.24, + "night": 280.77, + "eve": 285.15, + "morn": 281.38 + }, + "feels_like": { + "day": 282.67, + "night": 281.38, + "eve": 284.55, + "morn": 281.38 + }, + "pressure": 1016, + "humidity": 90, + "dew_point": 280.6, + "wind_speed": 2.15, + "wind_deg": 69, + "wind_gust": 5.06, + "weather": [ + { + "id": 501, + "main": "Rain", + "description": "moderate rain", + "icon": "10d" + } + ], + "clouds": 100, + "pop": 0.95, + "rain": 6.74, + "uvi": 6 + }, + { + "dt": 1621249200, + "sunrise": 1621222527, + "sunset": 1621278607, + "moonrise": 1621236780, + "moonset": 1621208820, + "moon_phase": 0.17, + "temp": { + "day": 284.85, + "min": 280.45, + "max": 287.32, + "night": 281.18, + "eve": 285.22, + "morn": 281.86 + }, + "feels_like": { + "day": 284.5, + "night": 281.86, + "eve": 284.75, + "morn": 281.86 + }, + "pressure": 1011, + "humidity": 93, + "dew_point": 283.09, + "wind_speed": 4.11, + "wind_deg": 244, + "wind_gust": 7.75, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "clouds": 100, + "pop": 1, + "rain": 7.89, + "uvi": 6 + } + ], + "alerts": [ + { + "sender_name": "Deutscher Wetterdienst", + "event": "WINDBÖEN", + "start": 1620619200, + "end": 1620673200, + "description": "Es treten oberhalb 400 m Windböen mit Geschwindigkeiten bis 60 km/h (17m/s, 33kn, Bft 7) aus südwestlicher Richtung auf. In exponierten Lagen muss mit Sturmböen bis 70 km/h (20m/s, 38kn, Bft 8) gerechnet werden." + }, + { + "sender_name": "Deutscher Wetterdienst", + "event": "wind gusts", + "start": 1620612000, + "end": 1620662400, + "description": "There is a risk of wind gusts (level 1 of 4).\nMax. gusts: \u003c 60 km/h; Wind direction: south-west; Increased gusts: in exposed locations \u003c 70 km/h" + } + ] +} diff --git a/bundles/org.openhab.binding.ems/weather3.json b/bundles/org.openhab.binding.ems/weather3.json new file mode 100644 index 0000000000000..7fe9d530bceb5 --- /dev/null +++ b/bundles/org.openhab.binding.ems/weather3.json @@ -0,0 +1,1777 @@ +{ + "lat": 50.55, + "lon": 8.43, + "timezone": "Europe/Berlin", + "timezone_offset": 7200, + "current": { + "dt": 1620648491, + "sunrise": 1620618337, + "sunset": 1620673188, + "temp": 291.86, + "feels_like": 291.27, + "pressure": 1009, + "humidity": 57, + "dew_point": 283.18, + "uvi": 5.01, + "clouds": 99, + "visibility": 10000, + "wind_speed": 2.24, + "wind_deg": 227, + "wind_gust": 4.92, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "rain": { + "1h": 0.56 + } + }, + "minutely": [ + { + "dt": 1620648540, + "precipitation": 0.562 + }, + { + "dt": 1620648600, + "precipitation": 0.562 + }, + { + "dt": 1620648660, + "precipitation": 0.5994 + }, + { + "dt": 1620648720, + "precipitation": 0.6368 + }, + { + "dt": 1620648780, + "precipitation": 0.6742 + }, + { + "dt": 1620648840, + "precipitation": 0.7116 + }, + { + "dt": 1620648900, + "precipitation": 0.749 + }, + { + "dt": 1620648960, + "precipitation": 0.799 + }, + { + "dt": 1620649020, + "precipitation": 0.849 + }, + { + "dt": 1620649080, + "precipitation": 0.899 + }, + { + "dt": 1620649140, + "precipitation": 0.949 + }, + { + "dt": 1620649200, + "precipitation": 0.999 + }, + { + "dt": 1620649260, + "precipitation": 0.999 + }, + { + "dt": 1620649320, + "precipitation": 0.999 + }, + { + "dt": 1620649380, + "precipitation": 0.999 + }, + { + "dt": 1620649440, + "precipitation": 0.999 + }, + { + "dt": 1620649500, + "precipitation": 0.999 + }, + { + "dt": 1620649560, + "precipitation": 0.9288 + }, + { + "dt": 1620649620, + "precipitation": 0.8586 + }, + { + "dt": 1620649680, + "precipitation": 0.7884 + }, + { + "dt": 1620649740, + "precipitation": 0.7182 + }, + { + "dt": 1620649800, + "precipitation": 0.648 + }, + { + "dt": 1620649860, + "precipitation": 0.5914 + }, + { + "dt": 1620649920, + "precipitation": 0.5348 + }, + { + "dt": 1620649980, + "precipitation": 0.4782 + }, + { + "dt": 1620650040, + "precipitation": 0.4216 + }, + { + "dt": 1620650100, + "precipitation": 0.365 + }, + { + "dt": 1620650160, + "precipitation": 0.3762 + }, + { + "dt": 1620650220, + "precipitation": 0.3874 + }, + { + "dt": 1620650280, + "precipitation": 0.3986 + }, + { + "dt": 1620650340, + "precipitation": 0.4098 + }, + { + "dt": 1620650400, + "precipitation": 0.421 + }, + { + "dt": 1620650460, + "precipitation": 0.4664 + }, + { + "dt": 1620650520, + "precipitation": 0.5118 + }, + { + "dt": 1620650580, + "precipitation": 0.5572 + }, + { + "dt": 1620650640, + "precipitation": 0.6026 + }, + { + "dt": 1620650700, + "precipitation": 0.648 + }, + { + "dt": 1620650760, + "precipitation": 0.7182 + }, + { + "dt": 1620650820, + "precipitation": 0.7884 + }, + { + "dt": 1620650880, + "precipitation": 0.8586 + }, + { + "dt": 1620650940, + "precipitation": 0.9288 + }, + { + "dt": 1620651000, + "precipitation": 0.999 + }, + { + "dt": 1620651060, + "precipitation": 1.1068 + }, + { + "dt": 1620651120, + "precipitation": 1.2146 + }, + { + "dt": 1620651180, + "precipitation": 1.3224 + }, + { + "dt": 1620651240, + "precipitation": 1.4302 + }, + { + "dt": 1620651300, + "precipitation": 1.538 + }, + { + "dt": 1620651360, + "precipitation": 1.538 + }, + { + "dt": 1620651420, + "precipitation": 1.538 + }, + { + "dt": 1620651480, + "precipitation": 1.538 + }, + { + "dt": 1620651540, + "precipitation": 1.538 + }, + { + "dt": 1620651600, + "precipitation": 1.538 + }, + { + "dt": 1620651660, + "precipitation": 1.5856 + }, + { + "dt": 1620651720, + "precipitation": 1.6332 + }, + { + "dt": 1620651780, + "precipitation": 1.6808 + }, + { + "dt": 1620651840, + "precipitation": 1.7284 + }, + { + "dt": 1620651900, + "precipitation": 1.776 + }, + { + "dt": 1620651960, + "precipitation": 1.6206 + }, + { + "dt": 1620652020, + "precipitation": 1.4652 + }, + { + "dt": 1620652080, + "precipitation": 1.3098 + }, + { + "dt": 1620652140, + "precipitation": 1.1544 + } + ], + "hourly": [ + { + "dt": 1620648000, + "temp": 291.86, + "feels_like": 291.27, + "pressure": 1009, + "humidity": 57, + "dew_point": 283.18, + "uvi": 5.01, + "clouds": 99, + "visibility": 10000, + "wind_speed": 5.42, + "wind_deg": 210, + "wind_gust": 8.69, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.36 + }, + { + "dt": 1620651600, + "temp": 291.3, + "feels_like": 290.82, + "pressure": 1009, + "humidity": 63, + "dew_point": 284.15, + "uvi": 2.14, + "clouds": 99, + "visibility": 10000, + "wind_speed": 3.41, + "wind_deg": 215, + "wind_gust": 6.65, + "weather": [ + { + "id": 501, + "main": "Rain", + "description": "moderate rain", + "icon": "10d" + } + ], + "pop": 0.61, + "rain": { + "1h": 1.54 + } + }, + { + "dt": 1620655200, + "temp": 290.9, + "feels_like": 290.45, + "pressure": 1009, + "humidity": 66, + "dew_point": 284.47, + "uvi": 1.64, + "clouds": 99, + "visibility": 10000, + "wind_speed": 3.65, + "wind_deg": 211, + "wind_gust": 5.87, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.49, + "rain": { + "1h": 0.56 + } + }, + { + "dt": 1620658800, + "temp": 289.43, + "feels_like": 289.07, + "pressure": 1010, + "humidity": 75, + "dew_point": 285, + "uvi": 1.08, + "clouds": 100, + "visibility": 10000, + "wind_speed": 3.35, + "wind_deg": 222, + "wind_gust": 7.4, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.6, + "rain": { + "1h": 0.15 + } + }, + { + "dt": 1620662400, + "temp": 288.23, + "feels_like": 288.01, + "pressure": 1010, + "humidity": 85, + "dew_point": 285.73, + "uvi": 0.6, + "clouds": 100, + "visibility": 10000, + "wind_speed": 2.24, + "wind_deg": 261, + "wind_gust": 6.35, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.66, + "rain": { + "1h": 0.32 + } + }, + { + "dt": 1620666000, + "temp": 286.27, + "feels_like": 286.17, + "pressure": 1011, + "humidity": 97, + "dew_point": 285.17, + "uvi": 0.25, + "clouds": 100, + "visibility": 6789, + "wind_speed": 1.39, + "wind_deg": 175, + "wind_gust": 2.77, + "weather": [ + { + "id": 501, + "main": "Rain", + "description": "moderate rain", + "icon": "10d" + } + ], + "pop": 0.7, + "rain": { + "1h": 1.65 + } + }, + { + "dt": 1620669600, + "temp": 285.62, + "feels_like": 285.46, + "pressure": 1010, + "humidity": 97, + "dew_point": 284.52, + "uvi": 0.07, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.56, + "wind_deg": 257, + "wind_gust": 2.32, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.78, + "rain": { + "1h": 0.74 + } + }, + { + "dt": 1620673200, + "temp": 285.06, + "feels_like": 284.87, + "pressure": 1010, + "humidity": 98, + "dew_point": 284.03, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.52, + "wind_deg": 302, + "wind_gust": 2.96, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10n" + } + ], + "pop": 1, + "rain": { + "1h": 0.67 + } + }, + { + "dt": 1620676800, + "temp": 284.81, + "feels_like": 284.59, + "pressure": 1011, + "humidity": 98, + "dew_point": 283.84, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 0.48, + "wind_deg": 184, + "wind_gust": 1.1, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10n" + } + ], + "pop": 1, + "rain": { + "1h": 0.62 + } + }, + { + "dt": 1620680400, + "temp": 284.57, + "feels_like": 284.33, + "pressure": 1011, + "humidity": 98, + "dew_point": 283.7, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 0.65, + "wind_deg": 322, + "wind_gust": 0.97, + "weather": [ + { + "id": 501, + "main": "Rain", + "description": "moderate rain", + "icon": "10n" + } + ], + "pop": 1, + "rain": { + "1h": 1.38 + } + }, + { + "dt": 1620684000, + "temp": 284.37, + "feels_like": 284.11, + "pressure": 1011, + "humidity": 98, + "dew_point": 283.55, + "uvi": 0, + "clouds": 100, + "visibility": 7586, + "wind_speed": 0.71, + "wind_deg": 265, + "wind_gust": 0.93, + "weather": [ + { + "id": 501, + "main": "Rain", + "description": "moderate rain", + "icon": "10n" + } + ], + "pop": 1, + "rain": { + "1h": 1.86 + } + }, + { + "dt": 1620687600, + "temp": 284.22, + "feels_like": 283.94, + "pressure": 1010, + "humidity": 98, + "dew_point": 283.38, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.31, + "wind_deg": 318, + "wind_gust": 3.13, + "weather": [ + { + "id": 501, + "main": "Rain", + "description": "moderate rain", + "icon": "10n" + } + ], + "pop": 1, + "rain": { + "1h": 1.22 + } + }, + { + "dt": 1620691200, + "temp": 284.08, + "feels_like": 283.79, + "pressure": 1010, + "humidity": 98, + "dew_point": 283.22, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.63, + "wind_deg": 304, + "wind_gust": 4.65, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10n" + } + ], + "pop": 1, + "rain": { + "1h": 0.37 + } + }, + { + "dt": 1620694800, + "temp": 284.08, + "feels_like": 283.76, + "pressure": 1010, + "humidity": 97, + "dew_point": 283.02, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.57, + "wind_deg": 283, + "wind_gust": 4.07, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10n" + } + ], + "pop": 0.69, + "rain": { + "1h": 0.14 + } + }, + { + "dt": 1620698400, + "temp": 283.82, + "feels_like": 283.48, + "pressure": 1009, + "humidity": 97, + "dew_point": 282.71, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.41, + "wind_deg": 284, + "wind_gust": 2.83, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0.65 + }, + { + "dt": 1620702000, + "temp": 283.57, + "feels_like": 283.2, + "pressure": 1009, + "humidity": 97, + "dew_point": 282.6, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 2.09, + "wind_deg": 274, + "wind_gust": 4.93, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10n" + } + ], + "pop": 0.73, + "rain": { + "1h": 0.33 + } + }, + { + "dt": 1620705600, + "temp": 283.43, + "feels_like": 283.05, + "pressure": 1009, + "humidity": 97, + "dew_point": 282.3, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 2.1, + "wind_deg": 280, + "wind_gust": 5.98, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.69 + }, + { + "dt": 1620709200, + "temp": 283.53, + "feels_like": 283.16, + "pressure": 1010, + "humidity": 97, + "dew_point": 282.38, + "uvi": 0.02, + "clouds": 100, + "visibility": 10000, + "wind_speed": 2.1, + "wind_deg": 282, + "wind_gust": 6.5, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.62 + }, + { + "dt": 1620712800, + "temp": 283.79, + "feels_like": 283.42, + "pressure": 1009, + "humidity": 96, + "dew_point": 282.57, + "uvi": 0.05, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.89, + "wind_deg": 308, + "wind_gust": 6.85, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.62 + }, + { + "dt": 1620716400, + "temp": 285.28, + "feels_like": 284.9, + "pressure": 1009, + "humidity": 90, + "dew_point": 283.16, + "uvi": 0.19, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.83, + "wind_deg": 308, + "wind_gust": 4.07, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.02 + }, + { + "dt": 1620720000, + "temp": 285.72, + "feels_like": 285.36, + "pressure": 1009, + "humidity": 89, + "dew_point": 283.46, + "uvi": 0.33, + "clouds": 100, + "visibility": 10000, + "wind_speed": 2.58, + "wind_deg": 311, + "wind_gust": 4.39, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1620723600, + "temp": 286.75, + "feels_like": 286.33, + "pressure": 1008, + "humidity": 83, + "dew_point": 283.42, + "uvi": 0.5, + "clouds": 100, + "visibility": 10000, + "wind_speed": 2.43, + "wind_deg": 20, + "wind_gust": 4.31, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1620727200, + "temp": 287.93, + "feels_like": 287.5, + "pressure": 1009, + "humidity": 78, + "dew_point": 283.46, + "uvi": 0.8, + "clouds": 100, + "visibility": 10000, + "wind_speed": 0.58, + "wind_deg": 357, + "wind_gust": 2.2, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1620730800, + "temp": 286.49, + "feels_like": 286.2, + "pressure": 1009, + "humidity": 89, + "dew_point": 284.16, + "uvi": 0.9, + "clouds": 100, + "visibility": 10000, + "wind_speed": 0.62, + "wind_deg": 296, + "wind_gust": 1.67, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.2, + "rain": { + "1h": 0.29 + } + }, + { + "dt": 1620734400, + "temp": 285.98, + "feels_like": 285.77, + "pressure": 1009, + "humidity": 94, + "dew_point": 284.37, + "uvi": 0.89, + "clouds": 100, + "visibility": 10000, + "wind_speed": 0.66, + "wind_deg": 333, + "wind_gust": 1.59, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.39, + "rain": { + "1h": 0.53 + } + }, + { + "dt": 1620738000, + "temp": 285.83, + "feels_like": 285.61, + "pressure": 1009, + "humidity": 94, + "dew_point": 284.34, + "uvi": 0.42, + "clouds": 100, + "visibility": 10000, + "wind_speed": 0.86, + "wind_deg": 359, + "wind_gust": 2.18, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.52, + "rain": { + "1h": 0.21 + } + }, + { + "dt": 1620741600, + "temp": 286.03, + "feels_like": 285.78, + "pressure": 1009, + "humidity": 92, + "dew_point": 284.22, + "uvi": 0.32, + "clouds": 100, + "visibility": 10000, + "wind_speed": 0.61, + "wind_deg": 7, + "wind_gust": 2.13, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.46 + }, + { + "dt": 1620745200, + "temp": 285.72, + "feels_like": 285.46, + "pressure": 1009, + "humidity": 93, + "dew_point": 284.09, + "uvi": 0.21, + "clouds": 100, + "visibility": 10000, + "wind_speed": 0.73, + "wind_deg": 41, + "wind_gust": 2, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "pop": 0.46, + "rain": { + "1h": 0.13 + } + }, + { + "dt": 1620748800, + "temp": 285.26, + "feels_like": 284.98, + "pressure": 1009, + "humidity": 94, + "dew_point": 283.76, + "uvi": 0.49, + "clouds": 100, + "visibility": 10000, + "wind_speed": 0.77, + "wind_deg": 36, + "wind_gust": 1.41, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.5 + }, + { + "dt": 1620752400, + "temp": 285.11, + "feels_like": 284.84, + "pressure": 1010, + "humidity": 95, + "dew_point": 283.69, + "uvi": 0.21, + "clouds": 100, + "visibility": 10000, + "wind_speed": 0.65, + "wind_deg": 328, + "wind_gust": 0.79, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.28 + }, + { + "dt": 1620756000, + "temp": 284.89, + "feels_like": 284.6, + "pressure": 1010, + "humidity": 95, + "dew_point": 283.47, + "uvi": 0.06, + "clouds": 100, + "visibility": 10000, + "wind_speed": 0.66, + "wind_deg": 358, + "wind_gust": 0.69, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.23 + }, + { + "dt": 1620759600, + "temp": 284.26, + "feels_like": 283.93, + "pressure": 1010, + "humidity": 96, + "dew_point": 283.01, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 0.1, + "wind_deg": 339, + "wind_gust": 0.53, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.11 + }, + { + "dt": 1620763200, + "temp": 284.05, + "feels_like": 283.7, + "pressure": 1011, + "humidity": 96, + "dew_point": 282.93, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 0.74, + "wind_deg": 235, + "wind_gust": 0.76, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0.11 + }, + { + "dt": 1620766800, + "temp": 284.08, + "feels_like": 283.74, + "pressure": 1011, + "humidity": 96, + "dew_point": 282.85, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 0.77, + "wind_deg": 247, + "wind_gust": 0.76, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0.09 + }, + { + "dt": 1620770400, + "temp": 283.67, + "feels_like": 283.31, + "pressure": 1010, + "humidity": 97, + "dew_point": 282.57, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.02, + "wind_deg": 258, + "wind_gust": 0.97, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0.09 + }, + { + "dt": 1620774000, + "temp": 282.99, + "feels_like": 282.99, + "pressure": 1011, + "humidity": 97, + "dew_point": 281.94, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.21, + "wind_deg": 256, + "wind_gust": 1.18, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0.06 + }, + { + "dt": 1620777600, + "temp": 282.74, + "feels_like": 282.74, + "pressure": 1011, + "humidity": 97, + "dew_point": 281.74, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 0.99, + "wind_deg": 248, + "wind_gust": 1.04, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0.06 + }, + { + "dt": 1620781200, + "temp": 282.3, + "feels_like": 282.3, + "pressure": 1011, + "humidity": 97, + "dew_point": 281.29, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.1, + "wind_deg": 248, + "wind_gust": 1.13, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1620784800, + "temp": 282.39, + "feels_like": 282.39, + "pressure": 1011, + "humidity": 97, + "dew_point": 281.34, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.17, + "wind_deg": 254, + "wind_gust": 1.11, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1620788400, + "temp": 282.81, + "feels_like": 282.54, + "pressure": 1011, + "humidity": 97, + "dew_point": 281.79, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.38, + "wind_deg": 257, + "wind_gust": 1.69, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04n" + } + ], + "pop": 0 + }, + { + "dt": 1620792000, + "temp": 282.98, + "feels_like": 282.59, + "pressure": 1011, + "humidity": 97, + "dew_point": 282.01, + "uvi": 0, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.51, + "wind_deg": 259, + "wind_gust": 2.36, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1620795600, + "temp": 283.58, + "feels_like": 283.21, + "pressure": 1011, + "humidity": 97, + "dew_point": 282.52, + "uvi": 0.03, + "clouds": 100, + "visibility": 10000, + "wind_speed": 1.4, + "wind_deg": 257, + "wind_gust": 5.13, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1620799200, + "temp": 284.32, + "feels_like": 283.97, + "pressure": 1011, + "humidity": 95, + "dew_point": 283.03, + "uvi": 0.09, + "clouds": 100, + "visibility": 10000, + "wind_speed": 2.32, + "wind_deg": 268, + "wind_gust": 6.01, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0 + }, + { + "dt": 1620802800, + "temp": 284.39, + "feels_like": 284.05, + "pressure": 1011, + "humidity": 95, + "dew_point": 283.04, + "uvi": 0.61, + "clouds": 100, + "visibility": 10000, + "wind_speed": 2.95, + "wind_deg": 277, + "wind_gust": 5.93, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.01 + }, + { + "dt": 1620806400, + "temp": 284.23, + "feels_like": 283.87, + "pressure": 1011, + "humidity": 95, + "dew_point": 282.9, + "uvi": 1.09, + "clouds": 100, + "visibility": 5163, + "wind_speed": 3.24, + "wind_deg": 281, + "wind_gust": 6.01, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.02 + }, + { + "dt": 1620810000, + "temp": 283.95, + "feels_like": 283.59, + "pressure": 1011, + "humidity": 96, + "dew_point": 282.73, + "uvi": 1.63, + "clouds": 100, + "visibility": 590, + "wind_speed": 3.33, + "wind_deg": 286, + "wind_gust": 6.76, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.16 + }, + { + "dt": 1620813600, + "temp": 284.19, + "feels_like": 283.8, + "pressure": 1011, + "humidity": 94, + "dew_point": 282.7, + "uvi": 1.61, + "clouds": 100, + "visibility": 3910, + "wind_speed": 3.53, + "wind_deg": 289, + "wind_gust": 6.77, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.34 + }, + { + "dt": 1620817200, + "temp": 284.67, + "feels_like": 284.23, + "pressure": 1011, + "humidity": 90, + "dew_point": 282.59, + "uvi": 1.82, + "clouds": 100, + "visibility": 10000, + "wind_speed": 4.02, + "wind_deg": 295, + "wind_gust": 6.45, + "weather": [ + { + "id": 804, + "main": "Clouds", + "description": "overcast clouds", + "icon": "04d" + } + ], + "pop": 0.34 + } + ], + "daily": [ + { + "dt": 1620644400, + "sunrise": 1620618337, + "sunset": 1620673188, + "moonrise": 1620617640, + "moonset": 1620668340, + "moon_phase": 0.96, + "temp": { + "day": 292.05, + "min": 284.57, + "max": 292.05, + "night": 284.57, + "eve": 286.27, + "morn": 288.11 + }, + "feels_like": { + "day": 291.51, + "night": 288.09, + "eve": 286.17, + "morn": 288.09 + }, + "pressure": 1009, + "humidity": 58, + "dew_point": 283.62, + "wind_speed": 5.42, + "wind_deg": 210, + "wind_gust": 11.48, + "weather": [ + { + "id": 501, + "main": "Rain", + "description": "moderate rain", + "icon": "10d" + } + ], + "clouds": 99, + "pop": 1, + "rain": 8.86, + "uvi": 5.03 + }, + { + "dt": 1620730800, + "sunrise": 1620704645, + "sunset": 1620759679, + "moonrise": 1620705000, + "moonset": 1620758820, + "moon_phase": 0, + "temp": { + "day": 286.49, + "min": 283.43, + "max": 287.93, + "night": 284.08, + "eve": 285.11, + "morn": 283.53 + }, + "feels_like": { + "day": 286.2, + "night": 283.16, + "eve": 284.84, + "morn": 283.16 + }, + "pressure": 1009, + "humidity": 89, + "dew_point": 284.16, + "wind_speed": 2.58, + "wind_deg": 311, + "wind_gust": 6.85, + "weather": [ + { + "id": 501, + "main": "Rain", + "description": "moderate rain", + "icon": "10d" + } + ], + "clouds": 100, + "pop": 1, + "rain": 5.08, + "uvi": 0.9 + }, + { + "dt": 1620817200, + "sunrise": 1620790954, + "sunset": 1620846169, + "moonrise": 1620792540, + "moonset": 1620849300, + "moon_phase": 0.02, + "temp": { + "day": 284.67, + "min": 282.3, + "max": 285.09, + "night": 283.05, + "eve": 284.4, + "morn": 283.58 + }, + "feels_like": { + "day": 284.23, + "night": 283.21, + "eve": 283.96, + "morn": 283.21 + }, + "pressure": 1011, + "humidity": 90, + "dew_point": 282.59, + "wind_speed": 4.02, + "wind_deg": 295, + "wind_gust": 6.77, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "clouds": 100, + "pop": 0.66, + "rain": 0.4, + "uvi": 1.82 + }, + { + "dt": 1620903600, + "sunrise": 1620877264, + "sunset": 1620932658, + "moonrise": 1620880380, + "moonset": 1620939720, + "moon_phase": 0.05, + "temp": { + "day": 283.28, + "min": 282.49, + "max": 285.55, + "night": 282.93, + "eve": 285.29, + "morn": 282.84 + }, + "feels_like": { + "day": 282.8, + "night": 282.84, + "eve": 284.75, + "morn": 282.84 + }, + "pressure": 1009, + "humidity": 94, + "dew_point": 281.78, + "wind_speed": 1.84, + "wind_deg": 76, + "wind_gust": 2.71, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "clouds": 100, + "pop": 0.88, + "rain": 2.24, + "uvi": 1.83 + }, + { + "dt": 1620990000, + "sunrise": 1620963577, + "sunset": 1621019147, + "moonrise": 1620968640, + "moonset": 0, + "moon_phase": 0.08, + "temp": { + "day": 288.88, + "min": 280.84, + "max": 289.48, + "night": 282.34, + "eve": 286.25, + "morn": 283.31 + }, + "feels_like": { + "day": 288.34, + "night": 282.71, + "eve": 285.89, + "morn": 282.71 + }, + "pressure": 1009, + "humidity": 70, + "dew_point": 282.95, + "wind_speed": 1.42, + "wind_deg": 29, + "wind_gust": 1.53, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "clouds": 99, + "pop": 0.99, + "rain": 4.46, + "uvi": 5.25 + }, + { + "dt": 1621076400, + "sunrise": 1621049892, + "sunset": 1621105635, + "moonrise": 1621057380, + "moonset": 1621029840, + "moon_phase": 0.11, + "temp": { + "day": 289.89, + "min": 279.6, + "max": 289.89, + "night": 280.88, + "eve": 283.71, + "morn": 283.11 + }, + "feels_like": { + "day": 289.45, + "night": 283.11, + "eve": 283.3, + "morn": 283.11 + }, + "pressure": 1011, + "humidity": 70, + "dew_point": 283.91, + "wind_speed": 4.57, + "wind_deg": 268, + "wind_gust": 6.74, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "clouds": 82, + "pop": 1, + "rain": 4.39, + "uvi": 6 + }, + { + "dt": 1621162800, + "sunrise": 1621136208, + "sunset": 1621192121, + "moonrise": 1621146780, + "moonset": 1621119600, + "moon_phase": 0.14, + "temp": { + "day": 283.85, + "min": 279.28, + "max": 286.59, + "night": 281.76, + "eve": 283.77, + "morn": 282.96 + }, + "feels_like": { + "day": 283.38, + "night": 282.96, + "eve": 283.32, + "morn": 282.96 + }, + "pressure": 1010, + "humidity": 92, + "dew_point": 281.96, + "wind_speed": 5.17, + "wind_deg": 238, + "wind_gust": 10.9, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "clouds": 95, + "pop": 0.98, + "rain": 4.05, + "uvi": 6 + }, + { + "dt": 1621249200, + "sunrise": 1621222527, + "sunset": 1621278607, + "moonrise": 1621236780, + "moonset": 1621208820, + "moon_phase": 0.17, + "temp": { + "day": 285.26, + "min": 279.41, + "max": 285.41, + "night": 279.41, + "eve": 283.03, + "morn": 281.49 + }, + "feels_like": { + "day": 284.67, + "night": 278.95, + "eve": 281.18, + "morn": 278.95 + }, + "pressure": 1015, + "humidity": 82, + "dew_point": 281.75, + "wind_speed": 5.53, + "wind_deg": 267, + "wind_gust": 10.12, + "weather": [ + { + "id": 500, + "main": "Rain", + "description": "light rain", + "icon": "10d" + } + ], + "clouds": 91, + "pop": 1, + "rain": 6.25, + "uvi": 6 + } + ], + "alerts": [ + { + "sender_name": "Deutscher Wetterdienst", + "event": "WINDBÖEN", + "start": 1620643620, + "end": 1620673200, + "description": "Es treten oberhalb 400 m Windböen mit Geschwindigkeiten bis 60 km/h (17m/s, 33kn, Bft 7) aus südwestlicher Richtung auf. In exponierten Lagen muss mit Sturmböen bis 70 km/h (20m/s, 38kn, Bft 8) gerechnet werden." + }, + { + "sender_name": "Deutscher Wetterdienst", + "event": "wind gusts", + "start": 1620612000, + "end": 1620662400, + "description": "There is a risk of wind gusts (level 1 of 4).\nMax. gusts: \u003c 60 km/h; Wind direction: south-west; Increased gusts: in exposed locations \u003c 70 km/h" + } + ] +} From d71dff8d620cd4532b19e1594567572baee0d5a1 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Wed, 12 May 2021 18:02:39 +0200 Subject: [PATCH 02/13] correct forecast date Signed-off-by: Bernd Weymann --- .../org/openhab/binding/ems/internal/handler/EMSHandler.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/handler/EMSHandler.java b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/handler/EMSHandler.java index a84e5098a7396..884ee7b12683c 100644 --- a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/handler/EMSHandler.java +++ b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/handler/EMSHandler.java @@ -106,8 +106,9 @@ public void generatePrediction() { Calendar date = Calendar.getInstance(); for (int days = 0; days < 7; days++) { int observationDay = date.get(Calendar.DAY_OF_MONTH); - String observationDaate = (date.get(Calendar.DAY_OF_MONTH) + 1) + "." + (date.get(Calendar.MONTH) + 1) + "." + String observationDaate = date.get(Calendar.DAY_OF_MONTH) + "." + (date.get(Calendar.MONTH) + 1) + "." + date.get(Calendar.YEAR); + logger.info("{}", observationDaate); double dailyProduction = 0; int dCounter = 0; double dCloduiness = 0; From 6ff13dfd4f009416075ede9b4e703648e4dc00fe Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Fri, 7 May 2021 13:46:22 +0200 Subject: [PATCH 03/13] add requested channel enhancements Signed-off-by: Bernd Weymann --- .../README.md | 47 +++++++++--- .../internal/ConnectedDriveConstants.java | 7 ++ .../dto/navigation/NavigationContainer.java | 14 ++++ .../internal/handler/ConnectedDriveProxy.java | 7 ++ .../handler/VehicleChannelHandler.java | 31 +++++++- .../internal/handler/VehicleHandler.java | 27 +++++++ .../internal/utils/Converter.java | 4 + .../i18n/bmwconnecteddrive_de.properties | 5 ++ .../OH-INF/thing/ev-range-channel-group.xml | 3 + .../OH-INF/thing/ev-vehicle-status-group.xml | 2 + .../thing/hybrid-range-channel-group.xml | 5 ++ .../OH-INF/thing/range-channel-types.xml | 20 +++++ .../thing/vehicle-status-channel-types.xml | 9 +++ .../OH-INF/thing/vehicle-status-group.xml | 1 + .../internal/dto/StatusWrapper.java | 75 +++++++++++++++++++ .../internal/handler/VehicleTests.java | 8 +- 16 files changed, 249 insertions(+), 16 deletions(-) create mode 100644 bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/navigation/NavigationContainer.java diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/README.md b/bundles/org.openhab.binding.bmwconnecteddrive/README.md index 2ed6771af160b..098c618990229 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/README.md +++ b/bundles/org.openhab.binding.bmwconnecteddrive/README.md @@ -147,8 +147,10 @@ Reflects overall status of the vehicle. | Next Service Date | service-date | DateTime | Date of upcoming service | | Mileage till Next Service | service-mileage | Number:Length | Mileage till upcoming service | | Check Control | check-control | String | Presence of active warning messages | +| Plug Connection Status | plug-connection | String | Only available for phev, bev_rex and bev | | Charging Status | charge | String | Only available for phev, bev_rex and bev | | Last Status Timestamp | last-update | DateTime | Date and time of last status update | +| Last Status Update Reason | last-update-reason | DateTime | Date and time of last status update | Overall Door Status values @@ -180,6 +182,26 @@ Charging Status values * _Charging Goal reached_ * _Waiting For Charging_ +Last update reasons + +* _CHARGING_DONE_ +* _CHARGING_INTERRUPED_ +* _CHARGING_PAUSED +* _CHARGING_STARTED_ +* _CYCLIC_RECHARGING_ +* _DISCONNECTED_ +* _DOOR_STATE_CHANGED_ +* _NO_CYCLIC_RECHARGING_ +* _NO_LSC_TRIGGER_ +* _ON_DEMAND_ +* _PREDICTION_UPDATE_ +* _TEMPORARY_POWER_SUPPLY_FAILURE_ +* _UNKNOWN_ +* _VEHICLE_MOVING_ +* _VEHICLE_SECURED_ +* _VEHICLE_SHUTDOWN_ +* _VEHICLE_SHUTDOWN_SECURED_ +* _VEHICLE_UNSECURED_ #### Services Group for all upcoming services with description, service date and/or service mileage. @@ -253,17 +275,20 @@ See description [Range vs Range Radius](#range-vs-range-radius) to get more info * Availability according to table * Read-only values -| Channel Label | Channel ID | Type | conv | phev | bev_rex | bev | -|-----------------------|-----------------------|----------------------|------|------|---------|-----| -| Mileage | mileage | Number:Length | X | X | X | X | -| Fuel Range | range-fuel | Number:Length | X | X | X | | -| Battery Range | range-electric | Number:Length | | X | X | X | -| Hybrid Range | range-hybrid | Number:Length | | X | X | | -| Battery Charge Level | soc | Number:Dimensionless | | X | X | X | -| Remaining Fuel | remaining-fuel | Number:Volume | X | X | X | | -| Fuel Range Radius | range-radius-fuel | Number:Length | X | X | X | | -| Electric Range Radius | range-radius-electric | Number:Length | | X | X | X | -| Hybrid Range Radius | range-radius-hybrid | Number:Length | | X | X | | +| Channel Label | Channel ID | Type | conv | phev | bev_rex | bev | +|---------------------------|-------------------------|----------------------|------|------|---------|-----| +| Mileage | mileage | Number:Length | X | X | X | X | +| Fuel Range | range-fuel | Number:Length | X | X | X | | +| Battery Range | range-electric | Number:Length | | X | X | X | +| Max Battery Range | range-electric-max | Number:Length | | X | X | X | +| Hybrid Range | range-hybrid | Number:Length | | X | X | | +| Battery Charge Level | soc | Number:Dimensionless | | X | X | X | +| Max Battery Capacity | soc-max | Number:Power | | | X | X | X | +| Remaining Fuel | remaining-fuel | Number:Volume | X | X | X | | +| Fuel Range Radius | range-radius-fuel | Number:Length | X | X | X | | +| Electric Range Radius | range-radius-electric | Number:Length | | X | X | X | +| Hybrid Range Radius | range-radius-hybrid | Number:Length | | X | X | | +| Max Hybrid Range Radius | range-radius-hybrid-max | Number:Length | | X | X | | #### Charge Profile diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/ConnectedDriveConstants.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/ConnectedDriveConstants.java index 31e22c4a09337..3e95c2c22aedf 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/ConnectedDriveConstants.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/ConnectedDriveConstants.java @@ -119,10 +119,12 @@ public enum ChargingPreference { public static final String SERVICE_DATE = "service-date"; public static final String SERVICE_MILEAGE = "service-mileage"; public static final String CHECK_CONTROL = "check-control"; + public static final String PLUG_CONNECTION = "plug-connection"; public static final String CHARGE_STATUS = "charge"; public static final String CHARGE_END_REASON = "reason"; public static final String CHARGE_REMAINING = "remaining"; public static final String LAST_UPDATE = "last-update"; + public static final String LAST_UPDATE_REASON = "last-update-reason"; // Door Details public static final String DOOR_DRIVER_FRONT = "driver-front"; @@ -161,13 +163,18 @@ public enum ChargingPreference { // Range public static final String RANGE_HYBRID = "hybrid"; + public static final String RANGE_HYBRID_MAX = "hybrid-max"; public static final String RANGE_ELECTRIC = "electric"; + public static final String RANGE_ELECTRIC_MAX = "electric-max"; public static final String SOC = "soc"; + public static final String SOC_MAX = "soc-max"; public static final String RANGE_FUEL = "fuel"; public static final String REMAINING_FUEL = "remaining-fuel"; public static final String RANGE_RADIUS_ELECTRIC = "radius-electric"; + public static final String RANGE_RADIUS_ELECTRIC_MAX = "radius-electric-max"; public static final String RANGE_RADIUS_FUEL = "radius-fuel"; public static final String RANGE_RADIUS_HYBRID = "radius-hybrid"; + public static final String RANGE_RADIUS_HYBRID_MAX = "radius-hybrid-max"; // Last Trip public static final String DURATION = "duration"; diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/navigation/NavigationContainer.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/navigation/NavigationContainer.java new file mode 100644 index 0000000000000..350f35d45f76f --- /dev/null +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/navigation/NavigationContainer.java @@ -0,0 +1,14 @@ +package org.openhab.binding.bmwconnecteddrive.internal.dto.navigation; + +public class NavigationContainer { + // "latitude": 56.789, + // "longitude": 8.765, + // "isoCountryCode": "DEU", + // "auxPowerRegular": 1.4, + // "auxPowerEcoPro": 1.2, + // "auxPowerEcoProPlus": 0.4, + // "soc": 25.952999114990234, + // "pendingUpdate": false, + // "vehicleTracking": true, + public double socmax;// ": 29.84 +} diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveProxy.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveProxy.java index af2164bf87b05..9499045bbd0d9 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveProxy.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveProxy.java @@ -73,6 +73,7 @@ public class ConnectedDriveProxy { final String baseUrl; final String vehicleUrl; final String legacyUrl; + final String navigationAPIUrl; final String vehicleStatusAPI = "/status"; final String lastTripAPI = "/statistics/lastTrip"; final String allTripsAPI = "/statistics/allTrips"; @@ -97,6 +98,7 @@ public ConnectedDriveProxy(HttpClientFactory httpClientFactory, ConnectedDriveCo vehicleUrl = "https://" + getRegionServer() + "/webapi/v1/user/vehicles"; baseUrl = vehicleUrl + "/"; legacyUrl = "https://" + getRegionServer() + "/api/vehicle/dynamic/v1/"; + navigationAPIUrl = "https://" + getRegionServer() + "/api/vehicle/navigation/"; } private synchronized void call(final String url, final boolean post, final @Nullable MultiMap params, @@ -181,6 +183,11 @@ public void requestLegacyVehcileStatus(VehicleConfiguration config, StringRespon get(legacyUrl + config.vin + "?offset=-60", null, callback); } + public void requestLNavigation(VehicleConfiguration config, StringResponseCallback callback) { + // see https://github.com/jupe76/bmwcdapi/search?q=dynamic%2Fv1 + get(navigationAPIUrl + config.vin, null, callback); + } + public void requestLastTrip(VehicleConfiguration config, StringResponseCallback callback) { get(baseUrl + config.vin + lastTripAPI, null, callback); } diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleChannelHandler.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleChannelHandler.java index 6ff369b1714b7..7a45b0e13d585 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleChannelHandler.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleChannelHandler.java @@ -417,6 +417,9 @@ protected void updateVehicleStatus(VehicleStatus vStatus) { // last update Time updateChannel(CHANNEL_GROUP_STATUS, LAST_UPDATE, DateTimeType.valueOf(Converter.getLocalDateTime(VehicleStatusUtils.getUpdateTime(vStatus)))); + // last update reason + updateChannel(CHANNEL_GROUP_STATUS, LAST_UPDATE_REASON, + StringType.valueOf(Converter.toTitleCase(vStatus.updateReason))); Doors doorState = null; try { @@ -442,7 +445,8 @@ protected void updateVehicleStatus(VehicleStatus vStatus) { // Range values // based on unit of length decide if range shall be reported in km or miles - float totalRange = 0; + double totalRange = 0; + double maxTotalRange = 0; if (isElectric) { totalRange += vStatus.remainingRangeElectric; QuantityType qtElectricRange = QuantityType.valueOf(vStatus.remainingRangeElectric, @@ -454,9 +458,21 @@ protected void updateVehicleStatus(VehicleStatus vStatus) { imperial ? Converter.getMiles(qtElectricRange) : qtElectricRange); updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_ELECTRIC, imperial ? Converter.getMiles(qtElectricRadius) : qtElectricRadius); + + maxTotalRange += vStatus.maxRangeElectric; + QuantityType qtMaxElectricRange = QuantityType.valueOf(vStatus.maxRangeElectric, + Constants.KILOMETRE_UNIT); + QuantityType qtMaxElectricRadius = QuantityType + .valueOf(Converter.guessRangeRadius(vStatus.maxRangeElectric), Constants.KILOMETRE_UNIT); + + updateChannel(CHANNEL_GROUP_RANGE, RANGE_ELECTRIC_MAX, + imperial ? Converter.getMiles(qtMaxElectricRange) : qtMaxElectricRange); + updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_ELECTRIC_MAX, + imperial ? Converter.getMiles(qtMaxElectricRadius) : qtMaxElectricRadius); } if (hasFuel) { totalRange += vStatus.remainingRangeFuel; + maxTotalRange += vStatus.remainingRangeFuel; QuantityType qtFuelRange = QuantityType.valueOf(vStatus.remainingRangeFuel, Constants.KILOMETRE_UNIT); QuantityType qtFuelRadius = QuantityType @@ -470,10 +486,17 @@ protected void updateVehicleStatus(VehicleStatus vStatus) { QuantityType qtHybridRange = QuantityType.valueOf(totalRange, Constants.KILOMETRE_UNIT); QuantityType qtHybridRadius = QuantityType.valueOf(Converter.guessRangeRadius(totalRange), Constants.KILOMETRE_UNIT); + QuantityType qtMaxHybridRange = QuantityType.valueOf(maxTotalRange, Constants.KILOMETRE_UNIT); + QuantityType qtMaxHybridRadius = QuantityType.valueOf(Converter.guessRangeRadius(maxTotalRange), + Constants.KILOMETRE_UNIT); updateChannel(CHANNEL_GROUP_RANGE, RANGE_HYBRID, imperial ? Converter.getMiles(qtHybridRange) : qtHybridRange); updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_HYBRID, imperial ? Converter.getMiles(qtHybridRadius) : qtHybridRadius); + updateChannel(CHANNEL_GROUP_RANGE, RANGE_HYBRID_MAX, + imperial ? Converter.getMiles(qtMaxHybridRange) : qtMaxHybridRange); + updateChannel(CHANNEL_GROUP_RANGE, RANGE_RADIUS_HYBRID_MAX, + imperial ? Converter.getMiles(qtMaxHybridRadius) : qtMaxHybridRadius); } updateChannel(CHANNEL_GROUP_RANGE, MILEAGE, @@ -488,6 +511,12 @@ protected void updateVehicleStatus(VehicleStatus vStatus) { // Charge Values if (isElectric) { + if (vStatus.connectionStatus != null) { + updateChannel(CHANNEL_GROUP_STATUS, PLUG_CONNECTION, + StringType.valueOf(Converter.toTitleCase(vStatus.connectionStatus))); + } else { + updateChannel(CHANNEL_GROUP_STATUS, PLUG_CONNECTION, UnDefType.NULL); + } if (vStatus.chargingStatus != null) { if (Constants.INVALID.equals(vStatus.chargingStatus)) { updateChannel(CHANNEL_GROUP_STATUS, CHARGE_STATUS, diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java index 035f0c1b6e74b..f2b8f1a594958 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java @@ -30,6 +30,7 @@ import org.openhab.binding.bmwconnecteddrive.internal.dto.DestinationContainer; import org.openhab.binding.bmwconnecteddrive.internal.dto.NetworkError; import org.openhab.binding.bmwconnecteddrive.internal.dto.compat.VehicleAttributesContainer; +import org.openhab.binding.bmwconnecteddrive.internal.dto.navigation.NavigationContainer; import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.AllTrips; import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.AllTripsContainer; import org.openhab.binding.bmwconnecteddrive.internal.dto.statistics.LastTrip; @@ -50,8 +51,10 @@ 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.QuantityType; import org.openhab.core.library.types.RawType; import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.Units; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; @@ -86,6 +89,7 @@ public class VehicleHandler extends VehicleChannelHandler { private ImageProperties imageProperties = new ImageProperties(); VehicleStatusCallback vehicleStatusCallback = new VehicleStatusCallback(); StringResponseCallback oldVehicleStatusCallback = new LegacyVehicleStatusCallback(); + StringResponseCallback navigationCallback = new NavigationStatusCallback(); StringResponseCallback lastTripCallback = new LastTripCallback(); StringResponseCallback allTripsCallback = new AllTripsCallback(); StringResponseCallback chargeProfileCallback = new ChargeProfilesCallback(); @@ -275,6 +279,8 @@ public void getData() { prox.requestVehcileStatus(config, vehicleStatusCallback); } addCallback(vehicleStatusCallback); + prox.requestLNavigation(config, navigationCallback); + addCallback(navigationCallback); if (isSupported(Constants.STATISTICS)) { prox.requestLastTrip(config, lastTripCallback); prox.requestAllTrips(config, allTripsCallback); @@ -682,6 +688,27 @@ public void onError(NetworkError error) { } } + public class NavigationStatusCallback implements StringResponseCallback { + @Override + public void onResponse(@Nullable String content) { + if (content != null) { + try { + NavigationContainer nav = Converter.getGson().fromJson(content, NavigationContainer.class); + + updateChannel(CHANNEL_GROUP_RANGE, SOC_MAX, QuantityType.valueOf(nav.socmax, Units.KILOWATT_HOUR)); + } catch (JsonSyntaxException jse) { + logger.debug("{}", jse.getMessage()); + } + } + } + + @Override + public void onError(NetworkError error) { + logger.debug("{}", error.toString()); + vehicleStatusCallback.onError(error); + } + } + private void handleChargeProfileCommand(ChannelUID channelUID, Command command) { if (chargeProfileEdit.isEmpty()) { chargeProfileEdit = getChargeProfileWrapper(); diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/Converter.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/Converter.java index bc6e3fc75bef6..4e783d137568f 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/Converter.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/Converter.java @@ -251,10 +251,14 @@ public static String transformLegacyStatus(@Nullable VehicleAttributesContainer vs.remainingRangeFuelMls = attributesMap.beRemainingRangeFuelMile; vs.remainingFuel = attributesMap.remainingFuel; vs.chargingLevelHv = attributesMap.chargingLevelHv; + vs.maxRangeElectric = attributesMap.beMaxRangeElectric; + vs.maxRangeElectricMls = attributesMap.beMaxRangeElectricMile; vs.chargingStatus = attributesMap.chargingHVStatus; + vs.connectionStatus = attributesMap.connectorStatus; vs.lastChargingEndReason = attributesMap.lastChargingEndReason; vs.updateTime = attributesMap.updateTimeConverted; + vs.updateReason = attributesMap.lastUpdateReason; Position p = new Position(); p.lat = attributesMap.gpsLat; diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/i18n/bmwconnecteddrive_de.properties b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/i18n/bmwconnecteddrive_de.properties index ab6509c50d7f1..25fa18a8c9dbc 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/i18n/bmwconnecteddrive_de.properties +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/i18n/bmwconnecteddrive_de.properties @@ -143,8 +143,10 @@ channel-type.bmwconnecteddrive.next-service-date-channel.label = N channel-type.bmwconnecteddrive.next-service-mileage-channel.label = N�chster Service in Kilometern channel-type.bmwconnecteddrive.check-control-channel.label = Warnung Aktiv channel-type.bmwconnecteddrive.charging-status-channel.label = Ladezustand +channel-type.bmwconnecteddrive.plug-connection-channel.label = Ladestecker channel-type.bmwconnecteddrive.charging-remaining-channel.label = Verbleibende Ladezeit channel-type.bmwconnecteddrive.last-update-channel.label = Letzte Aktualisierung +channel-type.bmwconnecteddrive.last-update-reason-channel.label = Grund der letzten Aktualisierung channel-type.bmwconnecteddrive.driver-front-channel.label = Fahrert�r channel-type.bmwconnecteddrive.driver-rear-channel.label = Fahrert�r Hinten @@ -162,12 +164,15 @@ channel-type.bmwconnecteddrive.sunroof-channel.label = Schiebedach channel-type.bmwconnecteddrive.mileage-channel.label = Tachostand channel-type.bmwconnecteddrive.range-hybrid-channel.label = Hybride Reichweite channel-type.bmwconnecteddrive.range-electric-channel.label = Elektrische Reichweite +channel-type.bmwconnecteddrive.range-electric-max-channel.label = Elektrische Reichweite bei voller Ladung channel-type.bmwconnecteddrive.soc-channel.label = Batterie Ladestand +channel-type.bmwconnecteddrive.soc-max-channel.label = Maximale Batteriekapazit�t channel-type.bmwconnecteddrive.range-fuel-channel.label = Verbrenner Reichweite channel-type.bmwconnecteddrive.remaining-fuel-channel.label = Tankstand channel-type.bmwconnecteddrive.range-radius-electric-channel.label = Elektrischer Reichweiten-Radius channel-type.bmwconnecteddrive.range-radius-fuel-channel.label = Verbrenner Reichweiten-Radius channel-type.bmwconnecteddrive.range-radius-hybrid-channel.label = Hybrider Reichweiten-Radius +channel-type.bmwconnecteddrive.range-radius-hybrid-max-channel.label = Hybrider Reichweiten-Radius bei voller Ladung channel-type.bmwconnecteddrive.service-name-channel.label = Service channel-type.bmwconnecteddrive.service-details-channel.label = Service Details diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/ev-range-channel-group.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/ev-range-channel-group.xml index c50b50633cda5..a0aeaa7b35e19 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/ev-range-channel-group.xml +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/ev-range-channel-group.xml @@ -9,8 +9,11 @@ + + + diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/ev-vehicle-status-group.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/ev-vehicle-status-group.xml index 74fbaa8c79a68..cbc5f820c3be3 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/ev-vehicle-status-group.xml +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/ev-vehicle-status-group.xml @@ -13,9 +13,11 @@ + + diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/hybrid-range-channel-group.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/hybrid-range-channel-group.xml index a260df0b9f6b2..3cdaecff5c5dd 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/hybrid-range-channel-group.xml +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/hybrid-range-channel-group.xml @@ -9,12 +9,17 @@ + + + + + diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/range-channel-types.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/range-channel-types.xml index b289d9bdeca85..3fb991d3cea36 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/range-channel-types.xml +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/range-channel-types.xml @@ -13,6 +13,11 @@ + + Number:Length + + + Number:Length @@ -28,12 +33,22 @@ + + Number:Power + + + Number:Volume + Number:Length + + + + Number:Length @@ -48,4 +63,9 @@ + + Number:Length + + + diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/vehicle-status-channel-types.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/vehicle-status-channel-types.xml index 1fa005aff175e..2f3a8a670cb81 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/vehicle-status-channel-types.xml +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/vehicle-status-channel-types.xml @@ -38,6 +38,11 @@ + + String + + + Number:Time @@ -48,4 +53,8 @@ + + String + + diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/vehicle-status-group.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/vehicle-status-group.xml index f8e99a6c407eb..36bcafd18a00b 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/vehicle-status-group.xml +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/vehicle-status-group.xml @@ -14,6 +14,7 @@ + diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/dto/StatusWrapper.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/dto/StatusWrapper.java index acd54c5478fb5..793307d9f1dc5 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/dto/StatusWrapper.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/dto/StatusWrapper.java @@ -164,6 +164,20 @@ private void checkResult(ChannelUID channelUID, State state) { ALLOWED_KM_ROUND_DEVIATION, "Mileage"); } break; + case RANGE_ELECTRIC_MAX: + assertTrue(isElectric, "Is Eelctric"); + assertTrue(state instanceof QuantityType); + qt = ((QuantityType) state); + if (imperial) { + assertEquals(ImperialUnits.MILE, qt.getUnit(), "Miles"); + assertEquals(Converter.round(qt.floatValue()), Converter.round(vStatus.maxRangeElectricMls), + ALLOWED_MILE_CONVERSION_DEVIATION, "Mileage"); + } else { + assertEquals(KILOMETRE, qt.getUnit(), "KM"); + assertEquals(Converter.round(qt.floatValue()), Converter.round(vStatus.maxRangeElectric), + ALLOWED_KM_ROUND_DEVIATION, "Mileage"); + } + break; case RANGE_FUEL: assertTrue(hasFuel, "Has Fuel"); if (!(state instanceof UnDefType)) { @@ -196,6 +210,22 @@ private void checkResult(ChannelUID channelUID, State state) { ALLOWED_KM_ROUND_DEVIATION, "Mileage"); } break; + case RANGE_HYBRID_MAX: + assertTrue(isHybrid, "Is Hybrid"); + assertTrue(state instanceof QuantityType); + qt = ((QuantityType) state); + if (imperial) { + assertEquals(ImperialUnits.MILE, qt.getUnit(), "Miles"); + assertEquals(Converter.round(qt.floatValue()), + Converter.round(vStatus.maxRangeElectricMls + vStatus.remainingRangeFuelMls), + ALLOWED_MILE_CONVERSION_DEVIATION, "Mileage"); + } else { + assertEquals(KILOMETRE, qt.getUnit(), "KM"); + assertEquals(Converter.round(qt.floatValue()), + Converter.round(vStatus.maxRangeElectric + vStatus.remainingRangeFuel), + ALLOWED_KM_ROUND_DEVIATION, "Mileage"); + } + break; case REMAINING_FUEL: assertTrue(hasFuel, "Has Fuel"); assertTrue(state instanceof QuantityType); @@ -212,6 +242,14 @@ private void checkResult(ChannelUID channelUID, State state) { assertEquals(Converter.round(vStatus.chargingLevelHv), Converter.round(qt.floatValue()), 0.01, "Charge Level"); break; + case SOC_MAX: + assertTrue(isElectric, "Is Eelctric"); + assertTrue(state instanceof QuantityType); + qt = ((QuantityType) state); + assertEquals(Units.KILOWATT_HOUR, qt.getUnit(), "kw/h"); + assertEquals(Converter.round(vStatus.chargingLevelHv), Converter.round(qt.floatValue()), 0.01, + "SOC Max"); + break; case LOCK: assertTrue(state instanceof StringType); st = (StringType) state; @@ -274,6 +312,12 @@ private void checkResult(ChannelUID channelUID, State state) { assertEquals(Units.MINUTE, qtt.getUnit(), "Minutes"); } break; + case PLUG_CONNECTION: + assertTrue(state instanceof StringType); + st = (StringType) state; + wanted = StringType.valueOf(Converter.toTitleCase(vStatus.connectionStatus)); + assertEquals(wanted.toString(), st.toString(), "Plug Connection State"); + break; case LAST_UPDATE: assertTrue(state instanceof DateTimeType); dtt = (DateTimeType) state; @@ -281,6 +325,12 @@ private void checkResult(ChannelUID channelUID, State state) { .valueOf(Converter.getLocalDateTime(VehicleStatusUtils.getUpdateTime(vStatus))); assertEquals(expected.toString(), dtt.toString(), "Last Update"); break; + case LAST_UPDATE_REASON: + assertTrue(state instanceof StringType); + st = (StringType) state; + wanted = StringType.valueOf(Converter.toTitleCase(vStatus.updateReason)); + assertEquals(wanted.toString(), st.toString(), "Last Update"); + break; case GPS: assertTrue(state instanceof PointType); pt = (PointType) state; @@ -306,6 +356,18 @@ private void checkResult(ChannelUID channelUID, State state) { "Range Radius Electric km"); } break; + case RANGE_RADIUS_ELECTRIC_MAX: + assertTrue(state instanceof QuantityType); + assertTrue(isElectric); + qt = (QuantityType) state; + if (imperial) { + assertEquals(Converter.guessRangeRadius(vStatus.maxRangeElectricMls), qt.floatValue(), 1, + "Range Radius Electric mi"); + } else { + assertEquals(Converter.guessRangeRadius(vStatus.maxRangeElectric), qt.floatValue(), 0.1, + "Range Radius Electric km"); + } + break; case RANGE_RADIUS_FUEL: assertTrue(state instanceof QuantityType); assertTrue(hasFuel); @@ -333,6 +395,19 @@ private void checkResult(ChannelUID channelUID, State state) { qt.floatValue(), ALLOWED_KM_ROUND_DEVIATION, "Range Radius Hybrid km"); } break; + case RANGE_RADIUS_HYBRID_MAX: + assertTrue(state instanceof QuantityType); + assertTrue(isHybrid); + qt = (QuantityType) state; + if (imperial) { + assertEquals( + Converter.guessRangeRadius(vStatus.maxRangeElectricMls + vStatus.remainingRangeFuelMls), + qt.floatValue(), ALLOWED_MILE_CONVERSION_DEVIATION, "Range Radius Hybrid Max mi"); + } else { + assertEquals(Converter.guessRangeRadius(vStatus.maxRangeElectric + vStatus.remainingRangeFuel), + qt.floatValue(), ALLOWED_KM_ROUND_DEVIATION, "Range Radius Hybrid Max km"); + } + break; case DOOR_DRIVER_FRONT: assertTrue(state instanceof StringType); st = (StringType) state; diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleTests.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleTests.java index 91221b930d572..78b2d14a6b83f 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleTests.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleTests.java @@ -51,11 +51,11 @@ public class VehicleTests { private final Logger logger = LoggerFactory.getLogger(VehicleHandler.class); - private static final int STATUS_ELECTRIC = 9; - private static final int STATUS_CONV = 7; - private static final int RANGE_HYBRID = 9; + private static final int STATUS_ELECTRIC = 12; + private static final int STATUS_CONV = 8; + private static final int RANGE_HYBRID = 12; private static final int RANGE_CONV = 4; - private static final int RANGE_ELECTRIC = 4; + private static final int RANGE_ELECTRIC = 5; private static final int DOORS = 12; private static final int CHECK_EMPTY = 3; private static final int CHECK_AVAILABLE = 3; From 7e3481044fcf3426d21e4b6aa15c423750e62fdc Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Fri, 7 May 2021 17:43:37 +0200 Subject: [PATCH 04/13] fixes after binding test in real environment Signed-off-by: Bernd Weymann --- .../dto/navigation/NavigationContainer.java | 17 +++++++++++++++++ .../handler/ConnectedDriveBridgeHandler.java | 4 +--- .../internal/handler/ConnectedDriveProxy.java | 2 +- .../internal/handler/VehicleChannelHandler.java | 6 +++--- .../internal/handler/VehicleHandler.java | 4 +++- .../OH-INF/i18n/bmwconnecteddrive_de.properties | 2 ++ .../OH-INF/thing/ev-range-channel-group.xml | 6 +++--- .../OH-INF/thing/hybrid-range-channel-group.xml | 12 ++++++------ .../OH-INF/thing/range-channel-types.xml | 5 +++++ 9 files changed, 41 insertions(+), 17 deletions(-) diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/navigation/NavigationContainer.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/navigation/NavigationContainer.java index 350f35d45f76f..8a78d9c953066 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/navigation/NavigationContainer.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/navigation/NavigationContainer.java @@ -1,5 +1,22 @@ +/** + * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.dto.navigation; +/** + * The {@link NavigationContainer} Data Transfer Object + * + * @author Bernd Weymann - Initial contribution + */ public class NavigationContainer { // "latitude": 56.789, // "longitude": 8.765, diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveBridgeHandler.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveBridgeHandler.java index 5c7ac485d5bfe..8e05933b4e368 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveBridgeHandler.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveBridgeHandler.java @@ -86,10 +86,8 @@ public void initialize() { public static boolean checkConfiguration(ConnectedDriveConfiguration config) { if (Constants.EMPTY.equals(config.userName) || Constants.EMPTY.equals(config.password)) { return false; - } else if (BimmerConstants.AUTH_SERVER_MAP.containsKey(config.region)) { - return true; } else { - return false; + return BimmerConstants.AUTH_SERVER_MAP.containsKey(config.region); } } diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveProxy.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveProxy.java index 9499045bbd0d9..79df74e09af92 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveProxy.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveProxy.java @@ -98,7 +98,7 @@ public ConnectedDriveProxy(HttpClientFactory httpClientFactory, ConnectedDriveCo vehicleUrl = "https://" + getRegionServer() + "/webapi/v1/user/vehicles"; baseUrl = vehicleUrl + "/"; legacyUrl = "https://" + getRegionServer() + "/api/vehicle/dynamic/v1/"; - navigationAPIUrl = "https://" + getRegionServer() + "/api/vehicle/navigation/"; + navigationAPIUrl = "https://" + getRegionServer() + "/api/vehicle/navigation/v1/"; } private synchronized void call(final String url, final boolean post, final @Nullable MultiMap params, diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleChannelHandler.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleChannelHandler.java index 7a45b0e13d585..d15a53402a5c5 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleChannelHandler.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleChannelHandler.java @@ -120,7 +120,7 @@ protected void updateChannel(final String group, final String id, final State st } protected void updateCheckControls(List ccl) { - if (ccl.size() == 0) { + if (ccl.isEmpty()) { // No Check Control available - show not active CCMMessage ccm = new CCMMessage(); ccm.ccmDescriptionLong = Constants.NO_ENTRIES; @@ -169,7 +169,7 @@ protected void selectCheckControl(int index) { protected void updateServices(List sl) { // if list is empty add "undefined" element - if (sl.size() == 0) { + if (sl.isEmpty()) { CBSMessage cbsm = new CBSMessage(); cbsm.cbsType = Constants.NO_ENTRIES; cbsm.cbsDescription = Constants.NO_ENTRIES; @@ -225,7 +225,7 @@ protected void selectService(int index) { protected void updateDestinations(List dl) { // if list is empty add "undefined" element - if (dl.size() == 0) { + if (dl.isEmpty()) { Destination dest = new Destination(); dest.city = Constants.NO_ENTRIES; dest.lat = -1; diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java index f2b8f1a594958..921de32142f9a 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java @@ -691,6 +691,7 @@ public void onError(NetworkError error) { public class NavigationStatusCallback implements StringResponseCallback { @Override public void onResponse(@Nullable String content) { + logger.info("{}", content); if (content != null) { try { NavigationContainer nav = Converter.getGson().fromJson(content, NavigationContainer.class); @@ -700,12 +701,13 @@ public void onResponse(@Nullable String content) { logger.debug("{}", jse.getMessage()); } } + removeCallback(this); } @Override public void onError(NetworkError error) { logger.debug("{}", error.toString()); - vehicleStatusCallback.onError(error); + removeCallback(this); } } diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/i18n/bmwconnecteddrive_de.properties b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/i18n/bmwconnecteddrive_de.properties index 25fa18a8c9dbc..b6306b789af7e 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/i18n/bmwconnecteddrive_de.properties +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/i18n/bmwconnecteddrive_de.properties @@ -163,6 +163,7 @@ channel-type.bmwconnecteddrive.sunroof-channel.label = Schiebedach channel-type.bmwconnecteddrive.mileage-channel.label = Tachostand channel-type.bmwconnecteddrive.range-hybrid-channel.label = Hybride Reichweite +channel-type.bmwconnecteddrive.range-hybrid-max-channel.label = Hybride Reichweite bei voller Ladung channel-type.bmwconnecteddrive.range-electric-channel.label = Elektrische Reichweite channel-type.bmwconnecteddrive.range-electric-max-channel.label = Elektrische Reichweite bei voller Ladung channel-type.bmwconnecteddrive.soc-channel.label = Batterie Ladestand @@ -170,6 +171,7 @@ channel-type.bmwconnecteddrive.soc-max-channel.label = Maximale Batteriekapazit channel-type.bmwconnecteddrive.range-fuel-channel.label = Verbrenner Reichweite channel-type.bmwconnecteddrive.remaining-fuel-channel.label = Tankstand channel-type.bmwconnecteddrive.range-radius-electric-channel.label = Elektrischer Reichweiten-Radius +channel-type.bmwconnecteddrive.range-radius-electric-max-channel.label = Elektrischer Reichweiten-Radius bei voller Ladung channel-type.bmwconnecteddrive.range-radius-fuel-channel.label = Verbrenner Reichweiten-Radius channel-type.bmwconnecteddrive.range-radius-hybrid-channel.label = Hybrider Reichweiten-Radius channel-type.bmwconnecteddrive.range-radius-hybrid-max-channel.label = Hybrider Reichweiten-Radius bei voller Ladung diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/ev-range-channel-group.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/ev-range-channel-group.xml index a0aeaa7b35e19..e73d81f78e6cf 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/ev-range-channel-group.xml +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/ev-range-channel-group.xml @@ -9,11 +9,11 @@ - + + + - - diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/hybrid-range-channel-group.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/hybrid-range-channel-group.xml index 3cdaecff5c5dd..44edc9a03119e 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/hybrid-range-channel-group.xml +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/hybrid-range-channel-group.xml @@ -9,17 +9,17 @@ - + + - - + - - - + + + diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/range-channel-types.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/range-channel-types.xml index 3fb991d3cea36..204052438b326 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/range-channel-types.xml +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/range-channel-types.xml @@ -28,6 +28,11 @@ + + Number:Length + + + Number:Dimensionless From c40ead95d033d931c035ac5084b3f3964a2ef6e1 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Sat, 8 May 2021 18:10:30 +0200 Subject: [PATCH 05/13] check java http oauth Signed-off-by: Bernd Weymann --- .../internal/handler/VehicleHandler.java | 2 +- .../internal/handler/AuthTest.java | 150 ++++++++++++++++++ 2 files changed, 151 insertions(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java index 921de32142f9a..d3ea480bb5845 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java @@ -706,7 +706,7 @@ public void onResponse(@Nullable String content) { @Override public void onError(NetworkError error) { - logger.debug("{}", error.toString()); + logger.info("{}", error.toString()); removeCallback(this); } } diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AuthTest.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AuthTest.java index 3b34f58610c05..af67660ffe060 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AuthTest.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AuthTest.java @@ -14,11 +14,23 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.*; +import static org.openhab.binding.bmwconnecteddrive.internal.utils.HTTPConstants.*; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.ProtocolException; +import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.Map; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.util.MultiMap; +import org.eclipse.jetty.util.UrlEncoded; import org.junit.jupiter.api.Test; import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConfiguration; import org.openhab.binding.bmwconnecteddrive.internal.utils.BimmerConstants; @@ -72,4 +84,142 @@ public void testRealTokenUpdate() { logger.info("Token {}", t.getBearerToken()); logger.info("Expires {}", t.isExpired()); } + + @Test + public void testJavaHttpAuth() { + ConnectedDriveConfiguration config = new ConnectedDriveConfiguration(); + config.region = BimmerConstants.REGION_ROW; + config.userName = "marika.weymann@gmail.com"; + config.password = "P4nd4b3r"; + + final StringBuilder legacyAuth = new StringBuilder(); + legacyAuth.append("https://"); + legacyAuth.append(BimmerConstants.AUTH_SERVER_MAP.get(config.region)); + legacyAuth.append(BimmerConstants.OAUTH_ENDPOINT); + URL url; + try { + + final MultiMap dataMap = new MultiMap(); + dataMap.add("grant_type", "password"); + dataMap.add(SCOPE, BimmerConstants.SCOPE_VALUES); + dataMap.add(USERNAME, config.userName); + dataMap.add(PASSWORD, config.password); + + String urlContent = UrlEncoded.encode(dataMap, StandardCharsets.UTF_8, false); + System.out.println(urlContent); + url = new URL(legacyAuth.toString() + "?" + urlContent); + System.out.println(url.toString()); + System.out.println(Integer.toString(urlContent.length())); + HttpURLConnection con = (HttpURLConnection) url.openConnection(); + con.setRequestMethod("POST"); + con.setRequestProperty(HttpHeader.CONTENT_LENGTH.asString(), Integer.toString(124)); + con.setRequestProperty(HttpHeader.CONTENT_TYPE.asString(), "application/x-www-form-urlencoded"); + // System.out.println(con.getHeaderField(HttpHeader.CONTENT_LENGTH.asString())); + // con.setRequestProperty(HttpHeader.CONNECTION.asString(), KEEP_ALIVE); + con.setRequestProperty(HttpHeader.HOST.asString(), BimmerConstants.SERVER_MAP.get(config.region)); + con.setRequestProperty(HttpHeader.AUTHORIZATION.asString(), + BimmerConstants.AUTHORIZATION_VALUE_MAP.get(config.region)); + con.setRequestProperty(CREDENTIALS, BimmerConstants.CREDENTIAL_VALUES); + con.setRequestProperty(HttpHeader.REFERER.asString(), BimmerConstants.REFERER_URL); + System.out.println(con.getHeaderFields()); + int status = con.getResponseCode(); + System.out.println("Status: " + status); + if (status < 400) { + BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); + String inputLine; + StringBuffer content = new StringBuffer(); + while ((inputLine = in.readLine()) != null) { + content.append(inputLine); + } + System.out.println("Content: " + content.toString()); + in.close(); + } + System.out.println(con.getContentLength()); + con.disconnect(); + } catch (MalformedURLException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (ProtocolException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + /** + * @Test + * public void testJavaHttpAuth2() { + * ConnectedDriveConfiguration config = new ConnectedDriveConfiguration(); + * config.region = BimmerConstants.REGION_ROW; + * config.userName = "marika.weymann@gmail.com"; + * config.password = "P4nd4b3r"; + * + * final StringBuilder legacyAuth = new StringBuilder(); + * legacyAuth.append("https://"); + * legacyAuth.append(BimmerConstants.AUTH_SERVER_MAP.get(config.region)); + * legacyAuth.append(BimmerConstants.OAUTH_ENDPOINT); + * URL url; + * try { + * + * final MultiMap dataMap = new MultiMap(); + * dataMap.add("grant_type", "password"); + * dataMap.add(SCOPE, BimmerConstants.SCOPE_VALUES); + * dataMap.add(USERNAME, config.userName); + * dataMap.add(PASSWORD, config.password); + * + * String urlContent = UrlEncoded.encode(dataMap, StandardCharsets.UTF_8, false); + * System.out.println(urlContent); + * url = new URL(legacyAuth.toString() + "?" + urlContent); + * System.out.println(url.toString()); + * System.out.println(Integer.toString(urlContent.length())); + * HttpURLConnection con = (HttpURLConnection) url.openConnection(); + * con.setRequestMethod("POST"); + * con.setRequestProperty(HttpHeader.CONTENT_TYPE.asString(), "application/x-www-form-urlencoded"); + * con.setRequestProperty(HttpHeader.CONTENT_LENGTH.asString(), Integer.toString(urlContent.length())); + * con.setRequestProperty(HttpHeader.CONNECTION.asString(), KEEP_ALIVE); + * con.setRequestProperty(HttpHeader.HOST.asString(), BimmerConstants.SERVER_MAP.get(config.region)); + * con.setRequestProperty(HttpHeader.AUTHORIZATION.asString(), + * BimmerConstants.AUTHORIZATION_VALUE_MAP.get(config.region)); + * con.setRequestProperty(CREDENTIALS, BimmerConstants.CREDENTIAL_VALUES); + * con.setRequestProperty(HttpHeader.REFERER.asString(), BimmerConstants.REFERER_URL); + * System.out.println(con.getHeaderField(HttpHeader.CONTENT_LENGTH.asString())); + * System.out.println(con.getHeaderFields()); + * int status = con.getResponseCode(); + * System.out.println("Status: " + status); + * if (status < 400) { + * BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); + * String inputLine; + * StringBuffer content = new StringBuffer(); + * while ((inputLine = in.readLine()) != null) { + * content.append(inputLine); + * } + * System.out.println("Content: " + content.toString()); + * in.close(); + * } + * System.out.println(con.getContentLength()); + * var client = new HttpClient(); + * + * // create a request + * var request = HttpRequest.newBuilder(URI.create("https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY")) + * .header("accept", "application/json").build(); + * + * // use the client to send the request + * var response = client.send(request, new JsonBodyHandler<>(APOD.class)); + * + * // the response: + * System.out.println(response.body().get().title); + * + * } catch (MalformedURLException e) { + * // TODO Auto-generated catch block + * e.printStackTrace(); + * } catch (ProtocolException e) { + * // TODO Auto-generated catch block + * e.printStackTrace(); + * } catch (IOException e) { + * // TODO Auto-generated catch block + * e.printStackTrace(); + * } + * } + */ } From 4dd9eb56eca58a1b2667194f20394f966b8d6c11 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Tue, 11 May 2021 08:58:08 +0200 Subject: [PATCH 06/13] rework oauth token access method Signed-off-by: Bernd Weymann --- .../internal/handler/ConnectedDriveProxy.java | 135 ++++++-- .../internal/handler/VehicleHandler.java | 2 +- .../internal/utils/BimmerConstants.java | 15 +- .../OH-INF/thing/range-channel-types.xml | 2 +- .../internal/handler/AuthProbes.java | 299 ++++++++++++++++++ .../internal/handler/AuthTest.java | 100 +----- 6 files changed, 444 insertions(+), 109 deletions(-) create mode 100644 bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AuthProbes.java diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveProxy.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveProxy.java index 79df74e09af92..8659d9905b2f3 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveProxy.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveProxy.java @@ -14,6 +14,13 @@ import static org.openhab.binding.bmwconnecteddrive.internal.utils.HTTPConstants.*; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -64,8 +71,10 @@ public class ConnectedDriveProxy { private final Token token = new Token(); private final HttpClient httpClient; private final HttpClient authHttpClient; + private final String authUri; private final String legacyAuthUri; private final ConnectedDriveConfiguration configuration; + private String clientId = "dbf0a542-ebd1-4ff0-a9a7-55172fbfce35"; /** * URLs taken from https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/const.py @@ -91,14 +100,15 @@ public ConnectedDriveProxy(HttpClientFactory httpClientFactory, ConnectedDriveCo configuration = config; final StringBuilder legacyAuth = new StringBuilder(); - legacyAuth.append("https://"); - legacyAuth.append(BimmerConstants.AUTH_SERVER_MAP.get(configuration.region)); - legacyAuth.append(BimmerConstants.OAUTH_ENDPOINT); - legacyAuthUri = legacyAuth.toString(); - vehicleUrl = "https://" + getRegionServer() + "/webapi/v1/user/vehicles"; + legacyAuthUri = "https://" + BimmerConstants.LEGACY_AUTH_SERVER_MAP.get(configuration.region) + + BimmerConstants.LEGACY_OAUTH_ENDPOINT; + authUri = "https://" + BimmerConstants.AUTH_SERVER_MAP.get(configuration.region) + + BimmerConstants.OAUTH_ENDPOINT; + vehicleUrl = "https://" + BimmerConstants.SERVER_MAP.get(configuration.region) + "/webapi/v1/user/vehicles"; baseUrl = vehicleUrl + "/"; - legacyUrl = "https://" + getRegionServer() + "/api/vehicle/dynamic/v1/"; - navigationAPIUrl = "https://" + getRegionServer() + "/api/vehicle/navigation/v1/"; + legacyUrl = "https://" + BimmerConstants.SERVER_MAP.get(configuration.region) + "/api/vehicle/dynamic/v1/"; + navigationAPIUrl = "https://" + BimmerConstants.SERVER_MAP.get(configuration.region) + + "/api/vehicle/navigation/v1/"; } private synchronized void call(final String url, final boolean post, final @Nullable MultiMap params, @@ -218,16 +228,6 @@ public void requestImage(VehicleConfiguration config, ImageProperties props, Byt get(localImageUrl, dataMap, callback); } - private String getRegionServer() { - final String retVal = BimmerConstants.SERVER_MAP.get(configuration.region); - return retVal == null ? Constants.INVALID : retVal; - } - - private String getAuthorizationValue() { - final String retVal = BimmerConstants.AUTHORIZATION_VALUE_MAP.get(configuration.region); - return retVal == null ? Constants.INVALID : retVal; - } - RemoteServiceHandler getRemoteServiceHandler(VehicleHandler vehicleHandler) { return new RemoteServiceHandler(vehicleHandler, this); } @@ -253,7 +253,7 @@ public Token getToken() { * * @return */ - private synchronized void updateToken() { + private synchronized void updateLegacyToken() { if (!authHttpClient.isStarted()) { try { authHttpClient.start(); @@ -265,8 +265,8 @@ private synchronized void updateToken() { final Request req = authHttpClient.POST(legacyAuthUri); req.header(HttpHeader.CONNECTION, KEEP_ALIVE); - req.header(HttpHeader.HOST, getRegionServer()); - req.header(HttpHeader.AUTHORIZATION, getAuthorizationValue()); + req.header(HttpHeader.HOST, BimmerConstants.SERVER_MAP.get(configuration.region)); + req.header(HttpHeader.AUTHORIZATION, BimmerConstants.AUTHORIZATION_VALUE_MAP.get(configuration.region)); req.header(CREDENTIALS, BimmerConstants.CREDENTIAL_VALUES); req.header(HttpHeader.REFERER, BimmerConstants.REFERER_URL); @@ -312,6 +312,101 @@ private synchronized void updateToken() { } } + /** + * Authorize at BMW Connected Drive Portal and get Token + * + * @return + */ + private synchronized void jettyUpdateToken() { + if (!authHttpClient.isStarted()) { + try { + authHttpClient.start(); + } catch (Exception e) { + logger.warn("Auth Http Client cannot be started {}", e.getMessage()); + return; + } + } + // POST("https://customer.bmwgroup.com/gcdm/oauth/authenticate"); + Request req = authHttpClient.POST(authUri); + + req.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED); + req.header(HttpHeader.CONNECTION, KEEP_ALIVE); + req.header(HttpHeader.HOST, BimmerConstants.SERVER_MAP.get(configuration.region)); + req.header(HttpHeader.AUTHORIZATION, BimmerConstants.AUTHORIZATION_VALUE_MAP.get(configuration.region)); + req.header(CREDENTIALS, BimmerConstants.CREDENTIAL_VALUES); + + MultiMap dataMap = new MultiMap(); + dataMap.add(CLIENT_ID, clientId); + dataMap.add(RESPONSE_TYPE, TOKEN); + dataMap.add(REDIRECT_URI, BimmerConstants.REDIRECT_URI_VALUE); + dataMap.add(SCOPE, BimmerConstants.SCOPE_VALUES); + dataMap.add(USERNAME, configuration.userName); + dataMap.add(PASSWORD, configuration.password); + String urlEncodedData = UrlEncoded.encode(dataMap, Charset.defaultCharset(), false); + req.header(CONTENT_LENGTH, Integer.toString(urlEncodedData.length())); + req.content(new StringContentProvider(urlEncodedData)); + try { + ContentResponse contentResponse = req.timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS).send(); + // Status needs to be 302 - Response is stored in Header + if (contentResponse.getStatus() == 302) { + HttpFields fields = contentResponse.getHeaders(); + HttpField field = fields.getField(HttpHeader.LOCATION); + tokenFromUrl(field.getValue()); + } else { + logger.debug("Authorization status {} reason {}", contentResponse.getStatus(), + contentResponse.getReason()); + } + } catch (InterruptedException | ExecutionException | TimeoutException e) { + logger.debug("Authorization exception: {}", e.getMessage()); + StackTraceElement[] trace = e.getStackTrace(); + for (int i = 0; i < trace.length; i++) { + logger.info("{}", trace[i]); + } + } + logger.info("updateToken - finish"); + } + + public synchronized void updateToken() { + try { + URL url = new URL(authUri); + HttpURLConnection.setFollowRedirects(false); + HttpURLConnection con = (HttpURLConnection) url.openConnection(); + con.setRequestMethod("POST"); + + con.setRequestProperty(HttpHeader.CONTENT_TYPE.toString(), CONTENT_TYPE_URL_ENCODED); + con.setRequestProperty(HttpHeader.CONNECTION.toString(), KEEP_ALIVE); + con.setRequestProperty(HttpHeader.HOST.toString(), + BimmerConstants.SERVER_MAP.get(BimmerConstants.SERVER_ROW)); + con.setRequestProperty(HttpHeader.AUTHORIZATION.toString(), + BimmerConstants.AUTHORIZATION_VALUE_MAP.get(configuration.region)); + con.setRequestProperty(CREDENTIALS, BimmerConstants.CREDENTIAL_VALUES); + con.setDoOutput(true); + + MultiMap dataMap = new MultiMap(); + dataMap.add(CLIENT_ID, clientId); + dataMap.add(RESPONSE_TYPE, TOKEN); + dataMap.add(REDIRECT_URI, BimmerConstants.REDIRECT_URI_VALUE); + dataMap.add(SCOPE, BimmerConstants.SCOPE_VALUES); + dataMap.add(USERNAME, configuration.userName); + dataMap.add(PASSWORD, configuration.password); + String urlEncodedData = UrlEncoded.encode(dataMap, Charset.defaultCharset(), false); + OutputStream os = con.getOutputStream(); + byte[] input = urlEncodedData.getBytes("utf-8"); + os.write(input, 0, input.length); + BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream(), "utf-8")); + StringBuilder response = new StringBuilder(); + String responseLine = null; + while ((responseLine = br.readLine()) != null) { + response.append(responseLine.trim()); + } + logger.info("Response Code {} Message {} ", con.getResponseCode(), con.getResponseMessage()); + tokenFromUrl(con.getHeaderField(HttpHeader.LOCATION.toString())); + } catch (IOException e) { + logger.warn("{}", e.getMessage()); + updateLegacyToken(); + } + } + void tokenFromUrl(String encodedUrl) { final MultiMap tokenMap = new MultiMap(); UrlEncoded.decodeTo(encodedUrl, tokenMap, StandardCharsets.US_ASCII); diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java index d3ea480bb5845..8cf1cec536af5 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java @@ -684,7 +684,7 @@ public void onResponse(@Nullable String content) { @Override public void onError(NetworkError error) { logger.debug("{}", error.toString()); - vehicleStatusCallback.onError(error); + // vehicleStatusCallback.onError(error); } } diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/BimmerConstants.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/BimmerConstants.java index 5c0ed6438faef..e3e9285107292 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/BimmerConstants.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/BimmerConstants.java @@ -35,12 +35,21 @@ public class BimmerConstants { // https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/country_selector.py public static final String AUTH_SERVER_NORTH_AMERICA = "b2vapi.bmwgroup.us/gcdm"; - public static final String AUTH_SERVER_CHINA = "b2vapi.bmwgroup.cn/gcdm"; - public static final String AUTH_SERVER_ROW = "b2vapi.bmwgroup.com/gcdm"; + public static final String AUTH_SERVER_CHINA = "customer.bmwgroup.cn/gcdm"; + public static final String AUTH_SERVER_ROW = "customer.bmwgroup.com/gcdm"; public static final Map AUTH_SERVER_MAP = Map.of(REGION_NORTH_AMERICA, AUTH_SERVER_NORTH_AMERICA, REGION_CHINA, AUTH_SERVER_CHINA, REGION_ROW, AUTH_SERVER_ROW); - public static final String OAUTH_ENDPOINT = "/oauth/token"; + // https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/country_selector.py + public static final String LEGACY_AUTH_SERVER_NORTH_AMERICA = "b2vapi.bmwgroup.us/gcdm"; + public static final String LEGACY_AUTH_SERVER_CHINA = "b2vapi.bmwgroup.cn/gcdm"; + public static final String LEGACY_AUTH_SERVER_ROW = "b2vapi.bmwgroup.com/gcdm"; + public static final Map LEGACY_AUTH_SERVER_MAP = Map.of(REGION_NORTH_AMERICA, + LEGACY_AUTH_SERVER_NORTH_AMERICA, REGION_CHINA, LEGACY_AUTH_SERVER_CHINA, REGION_ROW, + LEGACY_AUTH_SERVER_ROW); + + public static final String OAUTH_ENDPOINT = "/oauth/authenticate"; + public static final String LEGACY_OAUTH_ENDPOINT = "/oauth/token"; public static final String SERVER_NORTH_AMERICA = "b2vapi.bmwgroup.us"; public static final String SERVER_CHINA = "b2vapi.bmwgroup.cn:8592"; diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/range-channel-types.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/range-channel-types.xml index 204052438b326..6a2cfa730e87c 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/range-channel-types.xml +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/thing/range-channel-types.xml @@ -41,7 +41,7 @@ Number:Power - + Number:Volume diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AuthProbes.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AuthProbes.java new file mode 100644 index 0000000000000..4acdcdd3f1b02 --- /dev/null +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AuthProbes.java @@ -0,0 +1,299 @@ +/** + * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.handler; + +import static org.openhab.binding.bmwconnecteddrive.internal.utils.HTTPConstants.*; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.util.MultiMap; +import org.eclipse.jetty.util.UrlEncoded; +import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConfiguration; +import org.openhab.binding.bmwconnecteddrive.internal.dto.auth.AuthResponse; +import org.openhab.binding.bmwconnecteddrive.internal.utils.BimmerConstants; +import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants; +import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link AuthProbes} This class holds the important constants for the BMW Connected Drive Authorization. + * They + * are taken from the Bimmercode from github {@link https://github.com/bimmerconnected/bimmer_connected} + * File defining these constants + * {@link https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/account.py} + * https://customer.bmwgroup.com/one/app/oauth.js + * + * @author Bernd Weymann - Initial contribution + * https://github.com/weymann/openhab-addons/blob/80001e89d8b03ea633b2279dcff81f322a5ad6aa/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveProxy.java + * + */ +@NonNullByDefault +public class AuthProbes { + private final Logger logger = LoggerFactory.getLogger(AuthProbes.class); + private final Token token = new Token(); + private HttpClient httpClient; + private HttpClient authHttpClient; + private String authUri; + private String legacyAuthUri; + private ConnectedDriveConfiguration configuration; + private String clientId = "dbf0a542-ebd1-4ff0-a9a7-55172fbfce35"; + + /** + * URLs taken from https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/const.py + * + * """URLs for different services and error code mapping.""" + * + * AUTH_URL = 'https://customer.bmwgroup.com/{gcdm_oauth_endpoint}/authenticate' + * AUTH_URL_LEGACY = 'https://{server}/gcdm/oauth/token' + * BASE_URL = 'https://{server}/webapi/v1' + * + * VEHICLES_URL = BASE_URL + '/user/vehicles' + * VEHICLE_VIN_URL = VEHICLES_URL + '/{vin}' + * VEHICLE_STATUS_URL = VEHICLE_VIN_URL + '/status' + * REMOTE_SERVICE_STATUS_URL = VEHICLE_VIN_URL + '/serviceExecutionStatus?serviceType={service_type}' + * REMOTE_SERVICE_URL = VEHICLE_VIN_URL + "/executeService" + * VEHICLE_IMAGE_URL = VEHICLE_VIN_URL + "/image?width={width}&height={height}&view={view}" + * VEHICLE_POI_URL = VEHICLE_VIN_URL + '/sendpoi' + * + * } + */ + String baseUrl; + String legacyUrl; + String vehicleStatusAPI = "/status"; + String lastTripAPI = "/statistics/lastTrip"; + String allTripsAPI = "/statistics/allTrips"; + String chargeAPI = "/chargingprofile"; + String destinationAPI = "/destinations"; + String imageAPI = "/image"; + String rangeMapAPI = "/rangemap"; + String serviceExecutionAPI = "/executeService"; + String serviceExecutionStateAPI = "/serviceExecutionStatus"; + + public AuthProbes(HttpClientFactory httpClientFactory, ConnectedDriveConfiguration config) { + httpClient = httpClientFactory.getCommonHttpClient(); + authHttpClient = httpClientFactory.createHttpClient(AUTH_HTTP_CLIENT_NAME); + authHttpClient.setFollowRedirects(false); + try { + authHttpClient.start(); + } catch (Exception e) { + logger.warn("Auth Http Client cannot be started"); + } + configuration = config; + // generate URI for Authorization + // see https://customer.bmwgroup.com/one/app/oauth.js + StringBuilder uri = new StringBuilder(); + uri.append("https://customer.bmwgroup.com"); + if (BimmerConstants.SERVER_NORTH_AMERICA.equals(configuration.region)) { + uri.append("/gcdm/usa/oauth/authenticate"); + } else { + uri.append("/gcdm/oauth/authenticate"); + } + authUri = uri.toString(); + + StringBuilder legacyAuth = new StringBuilder(); + legacyAuth.append("https://"); + legacyAuth.append(BimmerConstants.AUTH_SERVER_MAP.get(configuration.region)); + legacyAuth.append(BimmerConstants.OAUTH_ENDPOINT); + legacyAuthUri = legacyAuth.toString(); + baseUrl = "https://" + getRegionServer() + "/webapi/v1/user/vehicles/"; + legacyUrl = "https://" + getRegionServer() + "/api/vehicle/dynamic/v1/"; + } + + private String getRegionServer() { + String retVal = BimmerConstants.SERVER_MAP.get(configuration.region); + if (retVal != null) { + return retVal; + } else { + return Constants.INVALID; + } + } + + /** + * Gets new token if old one is expired or invalid. In case of error the token remains. + * So if token refresh fails the corresponding requests will also fail and update the + * Thing status accordingly. + * + * @return token + */ + public Token getToken() { + if (token.isExpired() || !token.isValid()) { + legacyUpdateToken(); + } + return token; + } + + /** + * Authorize at BMW Connected Drive Portal and get Token + * + * @return + */ + private synchronized void jettyUpdateToken() { + logger.info("updateToken - start"); + Request req = authHttpClient.POST(authUri); + + req.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED); + req.header(HttpHeader.CONNECTION, KEEP_ALIVE); + req.header(HttpHeader.HOST, BimmerConstants.SERVER_MAP.get(configuration.region)); + req.header(HttpHeader.AUTHORIZATION, BimmerConstants.AUTHORIZATION_VALUE_MAP.get(configuration.region)); + req.header(CREDENTIALS, BimmerConstants.CREDENTIAL_VALUES); + + MultiMap dataMap = new MultiMap(); + dataMap.add(CLIENT_ID, clientId); + dataMap.add(RESPONSE_TYPE, TOKEN); + dataMap.add(REDIRECT_URI, BimmerConstants.REDIRECT_URI_VALUE); + dataMap.add(SCOPE, BimmerConstants.SCOPE_VALUES); + dataMap.add(USERNAME, configuration.userName); + dataMap.add(PASSWORD, configuration.password); + String urlEncodedData = UrlEncoded.encode(dataMap, Charset.defaultCharset(), false); + req.header(CONTENT_LENGTH, Integer.toString(urlEncodedData.length())); + req.content(new StringContentProvider(urlEncodedData)); + try { + ContentResponse contentResponse = req.timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS).send(); + // Status needs to be 302 - Response is stored in Header + if (contentResponse.getStatus() == 302) { + HttpFields fields = contentResponse.getHeaders(); + HttpField field = fields.getField(HttpHeader.LOCATION); + tokenFromUrl(field.getValue()); + } else { + logger.debug("Authorization status {} reason {}", contentResponse.getStatus(), + contentResponse.getReason()); + } + } catch (InterruptedException | ExecutionException | TimeoutException e) { + logger.debug("Authorization exception: {}", e.getMessage()); + StackTraceElement[] trace = e.getStackTrace(); + for (int i = 0; i < trace.length; i++) { + logger.info("{}", trace[i]); + } + } + logger.info("updateToken - finish"); + } + + public synchronized void updateToken() { + try { + URL url = new URL("https://customer.bmwgroup.com/gcdm/oauth/authenticate"); + HttpURLConnection.setFollowRedirects(false); + HttpURLConnection con = (HttpURLConnection) url.openConnection(); + con.setRequestMethod("POST"); + + con.setRequestProperty(HttpHeader.CONTENT_TYPE.toString(), CONTENT_TYPE_URL_ENCODED); + con.setRequestProperty(HttpHeader.CONNECTION.toString(), KEEP_ALIVE); + con.setRequestProperty(HttpHeader.HOST.toString(), + BimmerConstants.SERVER_MAP.get(BimmerConstants.SERVER_ROW)); + con.setRequestProperty(HttpHeader.AUTHORIZATION.toString(), + BimmerConstants.AUTHORIZATION_VALUE_MAP.get(configuration.region)); + con.setRequestProperty(CREDENTIALS, BimmerConstants.CREDENTIAL_VALUES); + con.setDoOutput(true); + + MultiMap dataMap = new MultiMap(); + dataMap.add(CLIENT_ID, clientId); + dataMap.add(RESPONSE_TYPE, TOKEN); + dataMap.add(REDIRECT_URI, BimmerConstants.REDIRECT_URI_VALUE); + dataMap.add(SCOPE, BimmerConstants.SCOPE_VALUES); + dataMap.add(USERNAME, configuration.userName); + dataMap.add(PASSWORD, configuration.password); + String urlEncodedData = UrlEncoded.encode(dataMap, Charset.defaultCharset(), false); + OutputStream os = con.getOutputStream(); + byte[] input = urlEncodedData.getBytes("utf-8"); + os.write(input, 0, input.length); + BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream(), "utf-8")); + StringBuilder response = new StringBuilder(); + String responseLine = null; + while ((responseLine = br.readLine()) != null) { + response.append(responseLine.trim()); + } + logger.info("Response Code {} Message {} ", con.getResponseCode(), con.getResponseMessage()); + tokenFromUrl(con.getHeaderField(HttpHeader.LOCATION.toString())); + } catch (IOException e) { + logger.warn("{}", e.getMessage()); + } + } + + private synchronized void legacyUpdateToken() { + try { + logger.info("Auth {}", legacyAuthUri); + URL url = new URL(legacyAuthUri); + HttpURLConnection.setFollowRedirects(false); + HttpURLConnection con = (HttpURLConnection) url.openConnection(); + con.setRequestMethod("POST"); + + con.setRequestProperty(HttpHeader.CONTENT_TYPE.toString(), CONTENT_TYPE_URL_ENCODED); + con.setRequestProperty(HttpHeader.CONNECTION.toString(), KEEP_ALIVE); + con.setRequestProperty(HttpHeader.HOST.toString(), + BimmerConstants.SERVER_MAP.get(BimmerConstants.SERVER_ROW)); + // con.setRequestProperty(HttpHeader.AUTHORIZATION.toString(), BimmerConstants.LEGACY_AUTHORIZATION_VALUE); + con.setRequestProperty(CREDENTIALS, BimmerConstants.LEGACY_CREDENTIAL_VALUES); + con.setDoOutput(true); + + MultiMap dataMap = new MultiMap(); + dataMap.add("grant_type", "password"); + dataMap.add(SCOPE, BimmerConstants.SCOPE_VALUES); + dataMap.add(USERNAME, configuration.userName); + dataMap.add(PASSWORD, configuration.password); + String urlEncodedData = UrlEncoded.encode(dataMap, Charset.defaultCharset(), false); + OutputStream os = con.getOutputStream(); + byte[] input = urlEncodedData.getBytes("utf-8"); + os.write(input, 0, input.length); + BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream(), "utf-8")); + StringBuilder response = new StringBuilder(); + String responseLine = null; + while ((responseLine = br.readLine()) != null) { + response.append(responseLine.trim()); + } + logger.info("Response Code {} Message {} ", con.getResponseCode(), con.getResponseMessage()); + // logger.info("Response {}", response.toString()); + AuthResponse authResponse = Converter.getGson().fromJson(response.toString(), AuthResponse.class); + // token.setToken(authResponse.access_token); + // token.setType(authResponse.token_type); + // token.setExpiration(authResponse.expires_in); + } catch (IOException e) { + logger.warn("{}", e.getMessage()); + } + } + + void tokenFromUrl(String encodedUrl) { + MultiMap tokenMap = new MultiMap(); + UrlEncoded.decodeTo(encodedUrl, tokenMap, StandardCharsets.US_ASCII); + tokenMap.forEach((key, value) -> { + if (value.size() > 0) { + String val = value.get(0); + if (key.endsWith(ACCESS_TOKEN)) { + token.setToken(val.toString()); + } else if (key.equals(EXPIRES_IN)) { + token.setExpiration(Integer.parseInt(val.toString())); + } else if (key.equals(TOKEN_TYPE)) { + token.setType(val.toString()); + } + } + }); + } +} diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AuthTest.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AuthTest.java index af67660ffe060..8329c5f13e824 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AuthTest.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AuthTest.java @@ -85,12 +85,11 @@ public void testRealTokenUpdate() { logger.info("Expires {}", t.isExpired()); } - @Test public void testJavaHttpAuth() { ConnectedDriveConfiguration config = new ConnectedDriveConfiguration(); config.region = BimmerConstants.REGION_ROW; - config.userName = "marika.weymann@gmail.com"; - config.password = "P4nd4b3r"; + config.userName = "bla"; + config.password = "bla"; final StringBuilder legacyAuth = new StringBuilder(); legacyAuth.append("https://"); @@ -137,89 +136,22 @@ public void testJavaHttpAuth() { System.out.println(con.getContentLength()); con.disconnect(); } catch (MalformedURLException e) { - // TODO Auto-generated catch block - e.printStackTrace(); } catch (ProtocolException e) { - // TODO Auto-generated catch block - e.printStackTrace(); } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); } } - /** - * @Test - * public void testJavaHttpAuth2() { - * ConnectedDriveConfiguration config = new ConnectedDriveConfiguration(); - * config.region = BimmerConstants.REGION_ROW; - * config.userName = "marika.weymann@gmail.com"; - * config.password = "P4nd4b3r"; - * - * final StringBuilder legacyAuth = new StringBuilder(); - * legacyAuth.append("https://"); - * legacyAuth.append(BimmerConstants.AUTH_SERVER_MAP.get(config.region)); - * legacyAuth.append(BimmerConstants.OAUTH_ENDPOINT); - * URL url; - * try { - * - * final MultiMap dataMap = new MultiMap(); - * dataMap.add("grant_type", "password"); - * dataMap.add(SCOPE, BimmerConstants.SCOPE_VALUES); - * dataMap.add(USERNAME, config.userName); - * dataMap.add(PASSWORD, config.password); - * - * String urlContent = UrlEncoded.encode(dataMap, StandardCharsets.UTF_8, false); - * System.out.println(urlContent); - * url = new URL(legacyAuth.toString() + "?" + urlContent); - * System.out.println(url.toString()); - * System.out.println(Integer.toString(urlContent.length())); - * HttpURLConnection con = (HttpURLConnection) url.openConnection(); - * con.setRequestMethod("POST"); - * con.setRequestProperty(HttpHeader.CONTENT_TYPE.asString(), "application/x-www-form-urlencoded"); - * con.setRequestProperty(HttpHeader.CONTENT_LENGTH.asString(), Integer.toString(urlContent.length())); - * con.setRequestProperty(HttpHeader.CONNECTION.asString(), KEEP_ALIVE); - * con.setRequestProperty(HttpHeader.HOST.asString(), BimmerConstants.SERVER_MAP.get(config.region)); - * con.setRequestProperty(HttpHeader.AUTHORIZATION.asString(), - * BimmerConstants.AUTHORIZATION_VALUE_MAP.get(config.region)); - * con.setRequestProperty(CREDENTIALS, BimmerConstants.CREDENTIAL_VALUES); - * con.setRequestProperty(HttpHeader.REFERER.asString(), BimmerConstants.REFERER_URL); - * System.out.println(con.getHeaderField(HttpHeader.CONTENT_LENGTH.asString())); - * System.out.println(con.getHeaderFields()); - * int status = con.getResponseCode(); - * System.out.println("Status: " + status); - * if (status < 400) { - * BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); - * String inputLine; - * StringBuffer content = new StringBuffer(); - * while ((inputLine = in.readLine()) != null) { - * content.append(inputLine); - * } - * System.out.println("Content: " + content.toString()); - * in.close(); - * } - * System.out.println(con.getContentLength()); - * var client = new HttpClient(); - * - * // create a request - * var request = HttpRequest.newBuilder(URI.create("https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY")) - * .header("accept", "application/json").build(); - * - * // use the client to send the request - * var response = client.send(request, new JsonBodyHandler<>(APOD.class)); - * - * // the response: - * System.out.println(response.body().get().title); - * - * } catch (MalformedURLException e) { - * // TODO Auto-generated catch block - * e.printStackTrace(); - * } catch (ProtocolException e) { - * // TODO Auto-generated catch block - * e.printStackTrace(); - * } catch (IOException e) { - * // TODO Auto-generated catch block - * e.printStackTrace(); - * } - * } - */ + + public void testCumstomerBMWAuthenticate() { + ConnectedDriveConfiguration c = new ConnectedDriveConfiguration(); + c.region = "ROW"; + c.userName = "bla"; + c.password = "bla"; + + HttpClient hc = mock(HttpClient.class); + HttpClientFactory hct = mock(HttpClientFactory.class); + when(hct.getCommonHttpClient()).thenReturn(hc); + when(hct.createHttpClient(AUTH_HTTP_CLIENT_NAME)).thenReturn(hc); + AuthProbes auth = new AuthProbes(hct, c); + auth.updateToken(); + } } From b80db09b6c0c9c6d98bf5844d5ff2de1dc777654 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Sun, 18 Jul 2021 21:09:31 +0200 Subject: [PATCH 07/13] Harmonize value map creation Signed-off-by: Bernd Weymann --- .../internal/handler/ConnectedDriveProxy.java | 72 +++++++++++-------- .../internal/handler/VehicleHandler.java | 2 +- 2 files changed, 45 insertions(+), 29 deletions(-) diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveProxy.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveProxy.java index 8659d9905b2f3..4e4f273c3aaaf 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveProxy.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveProxy.java @@ -20,7 +20,6 @@ import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; -import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -331,20 +330,33 @@ private synchronized void jettyUpdateToken() { req.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED); req.header(HttpHeader.CONNECTION, KEEP_ALIVE); - req.header(HttpHeader.HOST, BimmerConstants.SERVER_MAP.get(configuration.region)); + req.header(HttpHeader.HOST, BimmerConstants.AUTH_SERVER_MAP.get(configuration.region)); req.header(HttpHeader.AUTHORIZATION, BimmerConstants.AUTHORIZATION_VALUE_MAP.get(configuration.region)); req.header(CREDENTIALS, BimmerConstants.CREDENTIAL_VALUES); + logger.info("Post Uri: {}, Headers adapted {}", req.getURI(), req.getHeaders().size()); + + req.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, getAuthEncodedData(), StandardCharsets.UTF_8)); + // String urlEncodedData = UrlEncoded.encode(dataMap, Charset.defaultCharset(), false); + // req.header(CONTENT_LENGTH, Integer.toString(urlEncodedData.length())); + // req.content(new StringContentProvider(urlEncodedData)); + // logger.info("Header: {}, URL: {}", req.getHeaders(), urlEncodedData); + + // req.timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS).send(new BufferingResponseListener() { + // @NonNullByDefault({}) + // @Override + // public void onComplete(Result result) { + // if (result.getResponse().getStatus() == 302) { + // HttpFields fields = result.getResponse().getHeaders(); + // HttpField field = fields.getField(HttpHeader.LOCATION); + // tokenFromUrl(field.getValue()); + // logger.info("Jetty Auth succeeded!"); + // } else { + // logger.debug("Authorization status {} reason {}", result.getResponse().getStatus(), + // result.getResponse().getReason()); + // } + // } + // }); - MultiMap dataMap = new MultiMap(); - dataMap.add(CLIENT_ID, clientId); - dataMap.add(RESPONSE_TYPE, TOKEN); - dataMap.add(REDIRECT_URI, BimmerConstants.REDIRECT_URI_VALUE); - dataMap.add(SCOPE, BimmerConstants.SCOPE_VALUES); - dataMap.add(USERNAME, configuration.userName); - dataMap.add(PASSWORD, configuration.password); - String urlEncodedData = UrlEncoded.encode(dataMap, Charset.defaultCharset(), false); - req.header(CONTENT_LENGTH, Integer.toString(urlEncodedData.length())); - req.content(new StringContentProvider(urlEncodedData)); try { ContentResponse contentResponse = req.timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS).send(); // Status needs to be 302 - Response is stored in Header @@ -353,8 +365,8 @@ private synchronized void jettyUpdateToken() { HttpField field = fields.getField(HttpHeader.LOCATION); tokenFromUrl(field.getValue()); } else { - logger.debug("Authorization status {} reason {}", contentResponse.getStatus(), - contentResponse.getReason()); + logger.info("Authorization status {} reason {} content {}", contentResponse.getStatus(), + contentResponse.getHeaders(), new String(contentResponse.getContent(), StandardCharsets.UTF_8)); } } catch (InterruptedException | ExecutionException | TimeoutException e) { logger.debug("Authorization exception: {}", e.getMessage()); @@ -372,38 +384,31 @@ public synchronized void updateToken() { HttpURLConnection.setFollowRedirects(false); HttpURLConnection con = (HttpURLConnection) url.openConnection(); con.setRequestMethod("POST"); - con.setRequestProperty(HttpHeader.CONTENT_TYPE.toString(), CONTENT_TYPE_URL_ENCODED); con.setRequestProperty(HttpHeader.CONNECTION.toString(), KEEP_ALIVE); - con.setRequestProperty(HttpHeader.HOST.toString(), - BimmerConstants.SERVER_MAP.get(BimmerConstants.SERVER_ROW)); + con.setRequestProperty(HttpHeader.HOST.toString(), BimmerConstants.SERVER_MAP.get(configuration.region)); con.setRequestProperty(HttpHeader.AUTHORIZATION.toString(), BimmerConstants.AUTHORIZATION_VALUE_MAP.get(configuration.region)); con.setRequestProperty(CREDENTIALS, BimmerConstants.CREDENTIAL_VALUES); con.setDoOutput(true); - MultiMap dataMap = new MultiMap(); - dataMap.add(CLIENT_ID, clientId); - dataMap.add(RESPONSE_TYPE, TOKEN); - dataMap.add(REDIRECT_URI, BimmerConstants.REDIRECT_URI_VALUE); - dataMap.add(SCOPE, BimmerConstants.SCOPE_VALUES); - dataMap.add(USERNAME, configuration.userName); - dataMap.add(PASSWORD, configuration.password); - String urlEncodedData = UrlEncoded.encode(dataMap, Charset.defaultCharset(), false); + // logger.info("Header: {}, URL: {}", con.getHeaderFields(), urlEncodedData); + OutputStream os = con.getOutputStream(); - byte[] input = urlEncodedData.getBytes("utf-8"); + byte[] input = getAuthEncodedData().getBytes("utf-8"); os.write(input, 0, input.length); + BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream(), "utf-8")); StringBuilder response = new StringBuilder(); String responseLine = null; while ((responseLine = br.readLine()) != null) { response.append(responseLine.trim()); } - logger.info("Response Code {} Message {} ", con.getResponseCode(), con.getResponseMessage()); + logger.info("Headers adapted {}", con.getHeaderFields()); + logger.info("Auth Response Code {} Message {} ", con.getResponseCode(), con.getResponseMessage()); tokenFromUrl(con.getHeaderField(HttpHeader.LOCATION.toString())); } catch (IOException e) { logger.warn("{}", e.getMessage()); - updateLegacyToken(); } } @@ -423,4 +428,15 @@ void tokenFromUrl(String encodedUrl) { } }); } + + private String getAuthEncodedData() { + MultiMap dataMap = new MultiMap(); + dataMap.add(CLIENT_ID, clientId); + dataMap.add(RESPONSE_TYPE, TOKEN); + dataMap.add(REDIRECT_URI, BimmerConstants.REDIRECT_URI_VALUE); + dataMap.add(SCOPE, BimmerConstants.SCOPE_VALUES); + dataMap.add(USERNAME, configuration.userName); + dataMap.add(PASSWORD, configuration.password); + return UrlEncoded.encode(dataMap, StandardCharsets.UTF_8, false); + } } diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java index 8cf1cec536af5..6ef237f957d42 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java @@ -691,7 +691,7 @@ public void onError(NetworkError error) { public class NavigationStatusCallback implements StringResponseCallback { @Override public void onResponse(@Nullable String content) { - logger.info("{}", content); + logger.info("Navigation: {}", content); if (content != null) { try { NavigationContainer nav = Converter.getGson().fromJson(content, NavigationContainer.class); From 8458bb46e5d9f7be19e58916070bc7d5a226cb24 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Tue, 10 Aug 2021 18:06:08 +0200 Subject: [PATCH 08/13] move nav message to debug Signed-off-by: Bernd Weymann --- .../bmwconnecteddrive/internal/handler/VehicleHandler.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java index 6ef237f957d42..40639e683e7f6 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java @@ -691,7 +691,7 @@ public void onError(NetworkError error) { public class NavigationStatusCallback implements StringResponseCallback { @Override public void onResponse(@Nullable String content) { - logger.info("Navigation: {}", content); + logger.debug("Navigation: {}", content); if (content != null) { try { NavigationContainer nav = Converter.getGson().fromJson(content, NavigationContainer.class); @@ -706,7 +706,7 @@ public void onResponse(@Nullable String content) { @Override public void onError(NetworkError error) { - logger.info("{}", error.toString()); + logger.debug("{}", error.toString()); removeCallback(this); } } From cb2988c0c0591841ce3a4bdc9a127e171e614fab Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Sat, 18 Sep 2021 18:33:03 +0200 Subject: [PATCH 09/13] introduce preferMyBmw API Signed-off-by: Bernd Weymann --- .../README.md | 9 + .../internal/ConnectedDriveConfiguration.java | 5 + .../internal/dto/auth/AuthResponse.java | 5 + .../dto/remote/ExecutionStatusContainer.java | 3 + .../handler/ConnectedDriveBridgeHandler.java | 24 +- .../internal/handler/ConnectedDriveProxy.java | 352 +++++++++--------- .../handler/RemoteServiceHandler.java | 100 ++++- .../internal/handler/Token.java | 14 + .../internal/handler/VehicleHandler.java | 1 - .../internal/utils/BimmerConstants.java | 72 ++-- .../internal/utils/HTTPConstants.java | 5 +- .../resources/OH-INF/config/bridge-config.xml | 6 + .../i18n/bmwconnecteddrive_de.properties | 11 +- .../internal/handler/AuthProbes.java | 14 +- .../internal/handler/AuthTest.java | 138 +++++-- .../resources/responses/auth_response.json | 3 + .../resources/responses/tokenResponse.json | 8 + 17 files changed, 521 insertions(+), 249 deletions(-) create mode 100644 bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/auth_response.json create mode 100644 bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/tokenResponse.json diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/README.md b/bundles/org.openhab.binding.bmwconnecteddrive/README.md index 098c618990229..8036f5618593d 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/README.md +++ b/bundles/org.openhab.binding.bmwconnecteddrive/README.md @@ -95,6 +95,14 @@ The region Configuration has 3 different options * _CHINA_ * _ROW_ (Rest of World) + +#### Advanced Configuration + +| Parameter | Type | Description | +|-----------------|---------|--------------------------------------------------------------------| +| preferMyBmw | boolean | Prefer *MyBMW* API instead of *BMW Connected Drive* | + + ### Thing Configuration Same configuration is needed for all things @@ -202,6 +210,7 @@ Last update reasons * _VEHICLE_SHUTDOWN_ * _VEHICLE_SHUTDOWN_SECURED_ * _VEHICLE_UNSECURED_ + #### Services Group for all upcoming services with description, service date and/or service mileage. diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/ConnectedDriveConfiguration.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/ConnectedDriveConfiguration.java index c082bff346edb..ca17261734053 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/ConnectedDriveConfiguration.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/ConnectedDriveConfiguration.java @@ -37,4 +37,9 @@ public class ConnectedDriveConfiguration { * BMW Connected Drive Password */ public String password = Constants.EMPTY; + + /** + * Prefer MyBMW API instead of BMW Connected Drive + */ + public boolean preferMyBmw = false; } diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/auth/AuthResponse.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/auth/AuthResponse.java index 7363d49890583..ab5ce5063d9f2 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/auth/AuthResponse.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/auth/AuthResponse.java @@ -26,4 +26,9 @@ public class AuthResponse { public String tokenType; @SerializedName("expires_in") public int expiresIn; + + @Override + public String toString() { + return "Token " + accessToken + " type " + tokenType + " expires in " + expiresIn; + } } diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/remote/ExecutionStatusContainer.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/remote/ExecutionStatusContainer.java index eca56f1dd8adf..458138dd26a90 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/remote/ExecutionStatusContainer.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/dto/remote/ExecutionStatusContainer.java @@ -19,4 +19,7 @@ */ public class ExecutionStatusContainer { public ExecutionStatus executionStatus; + public String eventId; + public String creationTime; + public String eventStatus; } diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveBridgeHandler.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveBridgeHandler.java index 8e05933b4e368..9d35e53b18fd9 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveBridgeHandler.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveBridgeHandler.java @@ -16,6 +16,7 @@ import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.Optional; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -33,9 +34,11 @@ import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.binding.ThingHandlerService; import org.openhab.core.types.Command; import org.slf4j.Logger; @@ -73,13 +76,26 @@ public void initialize() { troubleshootFingerprint = Optional.empty(); updateStatus(ThingStatus.UNKNOWN); ConnectedDriveConfiguration config = getConfigAs(ConnectedDriveConfiguration.class); + logger.debug("Prefer MyBMW API {}", config.preferMyBmw); if (!checkConfiguration(config)) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR); } else { proxy = Optional.of(new ConnectedDriveProxy(httpClientFactory, config)); // give the system some time to create all predefined Vehicles // check with API call if bridge is online - initializerJob = Optional.of(scheduler.schedule(this::requestVehicles, 5, TimeUnit.SECONDS)); + initializerJob = Optional.of(scheduler.schedule(this::requestVehicles, 2, TimeUnit.SECONDS)); + Bridge b = super.getThing(); + List children = b.getThings(); + logger.debug("Update {} things", children.size()); + children.forEach(entry -> { + ThingHandler th = entry.getHandler(); + if (th != null) { + th.dispose(); + th.initialize(); + } else { + logger.debug("Handler is null"); + } + }); } } @@ -100,6 +116,7 @@ public void requestVehicles() { proxy.ifPresent(prox -> prox.requestVehicles(this)); } + // https://www.bmw-connecteddrive.de/api/me/vehicles/v2?all=true&brand=BM public String getDiscoveryFingerprint() { return troubleshootFingerprint.map(fingerprint -> { VehiclesContainer container = null; @@ -125,6 +142,8 @@ public String getDiscoveryFingerprint() { }); return Converter.getGson().toJson(container); } + } else { + logger.debug("container.vehicles is null"); } } } catch (JsonParseException jpe) { @@ -170,7 +189,8 @@ public void onResponse(@Nullable String response) { } }); } - return Converter.getGson().toJson(container); + } else { + troubleshootFingerprint = Optional.of(Constants.EMPTY_JSON); } } catch (JsonParseException jpe) { logger.debug("Fingerprint parse exception {}", jpe.getMessage()); diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveProxy.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveProxy.java index 4e4f273c3aaaf..90601c42758c0 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveProxy.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveProxy.java @@ -20,7 +20,10 @@ import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; +import java.net.URLDecoder; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -33,8 +36,6 @@ import org.eclipse.jetty.client.api.Result; import org.eclipse.jetty.client.util.BufferingResponseListener; import org.eclipse.jetty.client.util.StringContentProvider; -import org.eclipse.jetty.http.HttpField; -import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.util.MultiMap; import org.eclipse.jetty.util.UrlEncoded; @@ -51,8 +52,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.gson.JsonSyntaxException; - /** * The {@link ConnectedDriveProxy} This class holds the important constants for the BMW Connected Drive Authorization. * They @@ -67,11 +66,10 @@ @NonNullByDefault public class ConnectedDriveProxy { private final Logger logger = LoggerFactory.getLogger(ConnectedDriveProxy.class); + private Optional remoteServiceHandler = Optional.empty(); private final Token token = new Token(); private final HttpClient httpClient; private final HttpClient authHttpClient; - private final String authUri; - private final String legacyAuthUri; private final ConnectedDriveConfiguration configuration; private String clientId = "dbf0a542-ebd1-4ff0-a9a7-55172fbfce35"; @@ -81,6 +79,8 @@ public class ConnectedDriveProxy { final String baseUrl; final String vehicleUrl; final String legacyUrl; + final String remoteCommandUrl; + final String remoteStatusUrl; final String navigationAPIUrl; final String vehicleStatusAPI = "/status"; final String lastTripAPI = "/statistics/lastTrip"; @@ -91,27 +91,34 @@ public class ConnectedDriveProxy { final String rangeMapAPI = "/rangemap"; final String serviceExecutionAPI = "/executeService"; final String serviceExecutionStateAPI = "/serviceExecutionStatus"; + public final static String REMOTE_SERVICE_EADRAX_BASE_URL = "/eadrax-vrccs/v2/presentation/remote-commands/"; // '/{vin}/{service_type}' + final String REMOTE_SERVICE_EADRAX_STATUS_URL = REMOTE_SERVICE_EADRAX_BASE_URL + "eventStatus?eventId={event_id}"; + final String VEHICLE_EADRAX_POI_URL = "/eadrax-dcs/v1/send-to-car/send-to-car"; public ConnectedDriveProxy(HttpClientFactory httpClientFactory, ConnectedDriveConfiguration config) { httpClient = httpClientFactory.getCommonHttpClient(); authHttpClient = httpClientFactory.createHttpClient(AUTH_HTTP_CLIENT_NAME); - authHttpClient.setFollowRedirects(false); + if (!authHttpClient.isStarted()) { + try { + authHttpClient.start(); + } catch (Exception e) { + logger.debug("Auth client start failed"); + } + } configuration = config; - final StringBuilder legacyAuth = new StringBuilder(); - legacyAuthUri = "https://" + BimmerConstants.LEGACY_AUTH_SERVER_MAP.get(configuration.region) - + BimmerConstants.LEGACY_OAUTH_ENDPOINT; - authUri = "https://" + BimmerConstants.AUTH_SERVER_MAP.get(configuration.region) - + BimmerConstants.OAUTH_ENDPOINT; - vehicleUrl = "https://" + BimmerConstants.SERVER_MAP.get(configuration.region) + "/webapi/v1/user/vehicles"; + vehicleUrl = "https://" + BimmerConstants.API_SERVER_MAP.get(configuration.region) + "/webapi/v1/user/vehicles"; baseUrl = vehicleUrl + "/"; - legacyUrl = "https://" + BimmerConstants.SERVER_MAP.get(configuration.region) + "/api/vehicle/dynamic/v1/"; - navigationAPIUrl = "https://" + BimmerConstants.SERVER_MAP.get(configuration.region) + legacyUrl = "https://" + BimmerConstants.API_SERVER_MAP.get(configuration.region) + "/api/vehicle/dynamic/v1/"; + navigationAPIUrl = "https://" + BimmerConstants.API_SERVER_MAP.get(configuration.region) + "/api/vehicle/navigation/v1/"; + remoteCommandUrl = "https://" + BimmerConstants.EADRAX_SERVER_MAP.get(configuration.region) + + REMOTE_SERVICE_EADRAX_BASE_URL; + remoteStatusUrl = remoteCommandUrl + "eventStatus"; } - private synchronized void call(final String url, final boolean post, final @Nullable MultiMap params, - final ResponseCallback callback) { + public synchronized void call(final String url, final boolean post, final @Nullable String encoding, + final @Nullable String params, final ResponseCallback callback) { // only executed in "simulation mode" // SimulationTest.testSimulationOff() assures Injector is off when releasing if (Injector.isActive()) { @@ -125,22 +132,25 @@ private synchronized void call(final String url, final boolean post, final @Null return; } final Request req; - final String encoded = params == null || params.isEmpty() ? null - : UrlEncoded.encode(params, StandardCharsets.UTF_8, false); final String completeUrl; if (post) { completeUrl = url; req = httpClient.POST(url); - if (encoded != null) { - req.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, encoded, StandardCharsets.UTF_8)); + if (encoding != null) { + if (CONTENT_TYPE_URL_ENCODED.equals(encoding)) { + req.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, params, StandardCharsets.UTF_8)); + } else if (CONTENT_TYPE_JSON_ENCODED.equals(encoding)) { + req.header(HttpHeader.CONTENT_TYPE, encoding); + req.content(new StringContentProvider(CONTENT_TYPE_JSON_ENCODED, params, StandardCharsets.UTF_8)); + } } } else { - completeUrl = encoded == null ? url : url + Constants.QUESTION + encoded; + completeUrl = params == null ? url : url + Constants.QUESTION + params; req = httpClient.newRequest(completeUrl); } req.header(HttpHeader.AUTHORIZATION, getToken().getBearerToken()); - req.header(HttpHeader.REFERER, BimmerConstants.REFERER_URL); + req.header(HttpHeader.REFERER, BimmerConstants.LEGACY_REFERER_URL); req.timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS).send(new BufferingResponseListener() { @NonNullByDefault({}) @@ -171,51 +181,52 @@ public void onComplete(Result result) { }); } - public void get(String url, @Nullable MultiMap params, ResponseCallback callback) { - call(url, false, params, callback); + public void get(String url, @Nullable String coding, @Nullable String params, ResponseCallback callback) { + call(url, false, coding, params, callback); } - public void post(String url, @Nullable MultiMap params, ResponseCallback callback) { - call(url, true, params, callback); + public void post(String url, @Nullable String coding, @Nullable String params, ResponseCallback callback) { + call(url, true, coding, params, callback); } public void requestVehicles(StringResponseCallback callback) { - get(vehicleUrl, null, callback); + get(vehicleUrl, null, null, callback); } public void requestVehcileStatus(VehicleConfiguration config, StringResponseCallback callback) { - get(baseUrl + config.vin + vehicleStatusAPI, null, callback); + get(baseUrl + config.vin + vehicleStatusAPI, null, null, callback); } public void requestLegacyVehcileStatus(VehicleConfiguration config, StringResponseCallback callback) { // see https://github.com/jupe76/bmwcdapi/search?q=dynamic%2Fv1 - get(legacyUrl + config.vin + "?offset=-60", null, callback); + get(legacyUrl + config.vin + "?offset=-60", null, null, callback); } public void requestLNavigation(VehicleConfiguration config, StringResponseCallback callback) { // see https://github.com/jupe76/bmwcdapi/search?q=dynamic%2Fv1 - get(navigationAPIUrl + config.vin, null, callback); + get(navigationAPIUrl + config.vin, null, null, callback); } public void requestLastTrip(VehicleConfiguration config, StringResponseCallback callback) { - get(baseUrl + config.vin + lastTripAPI, null, callback); + get(baseUrl + config.vin + lastTripAPI, null, null, callback); } public void requestAllTrips(VehicleConfiguration config, StringResponseCallback callback) { - get(baseUrl + config.vin + allTripsAPI, null, callback); + get(baseUrl + config.vin + allTripsAPI, null, null, callback); } public void requestChargingProfile(VehicleConfiguration config, StringResponseCallback callback) { - get(baseUrl + config.vin + chargeAPI, null, callback); + get(baseUrl + config.vin + chargeAPI, null, null, callback); } public void requestDestinations(VehicleConfiguration config, StringResponseCallback callback) { - get(baseUrl + config.vin + destinationAPI, null, callback); + get(baseUrl + config.vin + destinationAPI, null, null, callback); } public void requestRangeMap(VehicleConfiguration config, @Nullable MultiMap params, StringResponseCallback callback) { - get(baseUrl + config.vin + rangeMapAPI, params, callback); + get(baseUrl + config.vin + rangeMapAPI, CONTENT_TYPE_URL_ENCODED, + UrlEncoded.encode(params, StandardCharsets.UTF_8, false), callback); } public void requestImage(VehicleConfiguration config, ImageProperties props, ByteResponseCallback callback) { @@ -224,11 +235,14 @@ public void requestImage(VehicleConfiguration config, ImageProperties props, Byt dataMap.add("width", Integer.toString(props.size)); dataMap.add("height", Integer.toString(props.size)); dataMap.add("view", props.viewport); - get(localImageUrl, dataMap, callback); + + get(localImageUrl, CONTENT_TYPE_URL_ENCODED, UrlEncoded.encode(dataMap, StandardCharsets.UTF_8, false), + callback); } RemoteServiceHandler getRemoteServiceHandler(VehicleHandler vehicleHandler) { - return new RemoteServiceHandler(vehicleHandler, this); + remoteServiceHandler = Optional.of(new RemoteServiceHandler(vehicleHandler, this)); + return remoteServiceHandler.get(); } // Token handling @@ -242,158 +256,141 @@ RemoteServiceHandler getRemoteServiceHandler(VehicleHandler vehicleHandler) { */ public Token getToken() { if (token.isExpired() || !token.isValid()) { - updateToken(); - } - return token; - } - - /** - * Authorize at BMW Connected Drive Portal and get Token - * - * @return - */ - private synchronized void updateLegacyToken() { - if (!authHttpClient.isStarted()) { - try { - authHttpClient.start(); - } catch (Exception e) { - logger.warn("Auth Http Client cannot be started {}", e.getMessage()); - return; - } - } - - final Request req = authHttpClient.POST(legacyAuthUri); - req.header(HttpHeader.CONNECTION, KEEP_ALIVE); - req.header(HttpHeader.HOST, BimmerConstants.SERVER_MAP.get(configuration.region)); - req.header(HttpHeader.AUTHORIZATION, BimmerConstants.AUTHORIZATION_VALUE_MAP.get(configuration.region)); - req.header(CREDENTIALS, BimmerConstants.CREDENTIAL_VALUES); - req.header(HttpHeader.REFERER, BimmerConstants.REFERER_URL); - - final MultiMap dataMap = new MultiMap(); - dataMap.add("grant_type", "password"); - dataMap.add(SCOPE, BimmerConstants.SCOPE_VALUES); - dataMap.add(USERNAME, configuration.userName); - dataMap.add(PASSWORD, configuration.password); - req.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, - UrlEncoded.encode(dataMap, StandardCharsets.UTF_8, false), StandardCharsets.UTF_8)); - try { - ContentResponse contentResponse = req.timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS).send(); - // Status needs to be 302 - Response is stored in Header - if (contentResponse.getStatus() == 302) { - final HttpFields fields = contentResponse.getHeaders(); - final HttpField field = fields.getField(HttpHeader.LOCATION); - tokenFromUrl(field.getValue()); - } else if (contentResponse.getStatus() == 200) { - final String stringContent = contentResponse.getContentAsString(); - if (stringContent != null && !stringContent.isEmpty()) { - try { - final AuthResponse authResponse = Converter.getGson().fromJson(stringContent, - AuthResponse.class); - if (authResponse != null) { - token.setToken(authResponse.accessToken); - token.setType(authResponse.tokenType); - token.setExpiration(authResponse.expiresIn); - } else { - logger.debug("not an Authorization response: {}", stringContent); - } - } catch (JsonSyntaxException jse) { - logger.debug("Authorization response unparsable: {}", stringContent); + if (configuration.preferMyBmw) { + if (!updateToken(authHttpClient)) { + if (!updateLegacyToken()) { + logger.debug("Authorization failed!"); } - } else { - logger.debug("Authorization response has no content"); } } else { - logger.debug("Authorization status {} reason {}", contentResponse.getStatus(), - contentResponse.getReason()); + if (!updateLegacyToken()) { + if (!updateToken(authHttpClient)) { + logger.debug("Authorization failed!"); + } + } } - } catch (InterruptedException | ExecutionException | TimeoutException e) { - logger.debug("Authorization exception: {}", e.getMessage()); } + remoteServiceHandler.ifPresent(serviceHandler -> { + serviceHandler.setMyBmwApiUsage(token.isMyBmwApiUsage()); + }); + return token; } - /** - * Authorize at BMW Connected Drive Portal and get Token - * - * @return - */ - private synchronized void jettyUpdateToken() { - if (!authHttpClient.isStarted()) { + public synchronized boolean updateToken(HttpClient client) { + if (!client.isStarted()) { try { - authHttpClient.start(); + client.start(); } catch (Exception e) { - logger.warn("Auth Http Client cannot be started {}", e.getMessage()); - return; + logger.debug("Authorization client cannot be started"); + return false; } } - // POST("https://customer.bmwgroup.com/gcdm/oauth/authenticate"); - Request req = authHttpClient.POST(authUri); - - req.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED); - req.header(HttpHeader.CONNECTION, KEEP_ALIVE); - req.header(HttpHeader.HOST, BimmerConstants.AUTH_SERVER_MAP.get(configuration.region)); - req.header(HttpHeader.AUTHORIZATION, BimmerConstants.AUTHORIZATION_VALUE_MAP.get(configuration.region)); - req.header(CREDENTIALS, BimmerConstants.CREDENTIAL_VALUES); - logger.info("Post Uri: {}, Headers adapted {}", req.getURI(), req.getHeaders().size()); - - req.content(new StringContentProvider(CONTENT_TYPE_URL_ENCODED, getAuthEncodedData(), StandardCharsets.UTF_8)); - // String urlEncodedData = UrlEncoded.encode(dataMap, Charset.defaultCharset(), false); - // req.header(CONTENT_LENGTH, Integer.toString(urlEncodedData.length())); - // req.content(new StringContentProvider(urlEncodedData)); - // logger.info("Header: {}, URL: {}", req.getHeaders(), urlEncodedData); - - // req.timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS).send(new BufferingResponseListener() { - // @NonNullByDefault({}) - // @Override - // public void onComplete(Result result) { - // if (result.getResponse().getStatus() == 302) { - // HttpFields fields = result.getResponse().getHeaders(); - // HttpField field = fields.getField(HttpHeader.LOCATION); - // tokenFromUrl(field.getValue()); - // logger.info("Jetty Auth succeeded!"); - // } else { - // logger.debug("Authorization status {} reason {}", result.getResponse().getStatus(), - // result.getResponse().getReason()); - // } - // } - // }); + if (BimmerConstants.REGION_CHINA.equals(configuration.region)) { + // region China stays on fallback solution + logger.debug("Region {} not supported yet for MyBMW Login", BimmerConstants.REGION_CHINA); + return false; + } + String authUri = "https://" + BimmerConstants.AUTH_SERVER_MAP.get(configuration.region) + + BimmerConstants.OAUTH_ENDPOINT; + + Request authRequest = client.POST(authUri); + authRequest.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED); + + MultiMap baseValues = new MultiMap(); + baseValues.add(CLIENT_ID, Constants.EMPTY + BimmerConstants.CLIENT_ID.get(configuration.region)); + baseValues.add(RESPONSE_TYPE, CODE); + baseValues.add(REDIRECT_URI, BimmerConstants.REDIRECT_URI_VALUE); + baseValues.add("state", Constants.EMPTY + BimmerConstants.STATE.get(configuration.region)); + baseValues.add("nonce", "login_nonce"); + baseValues.add(SCOPE, BimmerConstants.SCOPE_VALUES); + + MultiMap authValues = new MultiMap(); + authValues.add(GRANT_TYPE, "authorization_code"); + authValues.add(USERNAME, configuration.userName); + authValues.add(PASSWORD, configuration.password); + MultiMap authChallenge = new MultiMap(); + authChallenge.addAllValues(baseValues); + authChallenge.addAllValues(authValues); + + String authEncoded = UrlEncoded.encode(authChallenge, Charset.defaultCharset(), false); + authRequest.content(new StringContentProvider(authEncoded)); try { - ContentResponse contentResponse = req.timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS).send(); - // Status needs to be 302 - Response is stored in Header - if (contentResponse.getStatus() == 302) { - HttpFields fields = contentResponse.getHeaders(); - HttpField field = fields.getField(HttpHeader.LOCATION); - tokenFromUrl(field.getValue()); - } else { - logger.info("Authorization status {} reason {} content {}", contentResponse.getStatus(), - contentResponse.getHeaders(), new String(contentResponse.getContent(), StandardCharsets.UTF_8)); + ContentResponse authResponse = authRequest.timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS).send(); + String authResponseString = URLDecoder.decode(authResponse.getContentAsString(), Charset.defaultCharset()); + String[] keys = authResponseString.split("&"); + for (int i = 0; i < keys.length; i++) { + if (keys[i].startsWith(AUTHORIZATION)) { + String authCode = keys[i].split("=")[1]; + authCode = authCode.split("\"")[0]; + MultiMap codeChallenge = new MultiMap(); + codeChallenge.addAllValues(baseValues); + codeChallenge.put(AUTHORIZATION, authCode); + + Request codeRequest = client.POST(authUri).followRedirects(false); + codeRequest.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED); + // codeRequest.header("User-Agent", "okhttp/3.12.2"); + String codeEncoded = UrlEncoded.encode(codeChallenge, Charset.defaultCharset(), false); + // codeEncoded += "&authorization=" + UrlEncoded.encodeString(authCode); + codeRequest.content(new StringContentProvider(codeEncoded)); + ContentResponse codeResponse = codeRequest.timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS).send(); + String code = ConnectedDriveProxy.codeFromUrl(codeResponse.getHeaders().get(HttpHeader.LOCATION)); + + // Get Token + String tokenUrl = "https://" + BimmerConstants.AUTH_SERVER_MAP.get(configuration.region) + + BimmerConstants.TOKEN_ENDPOINT; + + Request tokenRequest = client.POST(tokenUrl).followRedirects(false); + tokenRequest.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED); + tokenRequest.header(HttpHeader.AUTHORIZATION, + BimmerConstants.AUTHORIZATION_VALUE_MAP.get(configuration.region)); + MultiMap tokenValues = new MultiMap(); + tokenValues.put(CODE, code); + tokenValues.put("code_verifier", + Constants.EMPTY + BimmerConstants.CODE_VERIFIER.get(configuration.region)); + tokenValues.put(REDIRECT_URI, BimmerConstants.REDIRECT_URI_VALUE); + tokenValues.put(GRANT_TYPE, "authorization_code"); + String tokenEncoded = UrlEncoded.encode(tokenValues, Charset.defaultCharset(), false); + tokenRequest.content(new StringContentProvider(tokenEncoded)); + ContentResponse tokenResponse = tokenRequest.timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS).send(); + AuthResponse authResponseJson = Converter.getGson().fromJson(tokenResponse.getContentAsString(), + AuthResponse.class); + token.setToken(authResponseJson.accessToken); + token.setType(authResponseJson.tokenType); + token.setExpiration(authResponseJson.expiresIn); + token.setMyBmwApiUsage(true); + return true; + } } } catch (InterruptedException | ExecutionException | TimeoutException e) { logger.debug("Authorization exception: {}", e.getMessage()); - StackTraceElement[] trace = e.getStackTrace(); - for (int i = 0; i < trace.length; i++) { - logger.info("{}", trace[i]); - } } - logger.info("updateToken - finish"); + return false; } - public synchronized void updateToken() { + public synchronized boolean updateLegacyToken() { + logger.debug("updateLegacyToken"); try { + /** + * The authorization with Jetty HttpClient doens't work anymore + * When calling Jetty with same headers and content a ConcurrentExcpetion is thrown + * So fallback legacy authorization will stay on java.net handling + */ + String authUri = "https://" + BimmerConstants.AUTH_SERVER_MAP.get(configuration.region) + + BimmerConstants.OAUTH_ENDPOINT; URL url = new URL(authUri); HttpURLConnection.setFollowRedirects(false); HttpURLConnection con = (HttpURLConnection) url.openConnection(); con.setRequestMethod("POST"); con.setRequestProperty(HttpHeader.CONTENT_TYPE.toString(), CONTENT_TYPE_URL_ENCODED); con.setRequestProperty(HttpHeader.CONNECTION.toString(), KEEP_ALIVE); - con.setRequestProperty(HttpHeader.HOST.toString(), BimmerConstants.SERVER_MAP.get(configuration.region)); + con.setRequestProperty(HttpHeader.HOST.toString(), + BimmerConstants.API_SERVER_MAP.get(configuration.region)); con.setRequestProperty(HttpHeader.AUTHORIZATION.toString(), - BimmerConstants.AUTHORIZATION_VALUE_MAP.get(configuration.region)); - con.setRequestProperty(CREDENTIALS, BimmerConstants.CREDENTIAL_VALUES); + BimmerConstants.LEGACY_AUTHORIZATION_VALUE_MAP.get(configuration.region)); + con.setRequestProperty(CREDENTIALS, BimmerConstants.LEGACY_CREDENTIAL_VALUES); con.setDoOutput(true); - // logger.info("Header: {}, URL: {}", con.getHeaderFields(), urlEncodedData); - OutputStream os = con.getOutputStream(); byte[] input = getAuthEncodedData().getBytes("utf-8"); os.write(input, 0, input.length); @@ -404,15 +401,16 @@ public synchronized void updateToken() { while ((responseLine = br.readLine()) != null) { response.append(responseLine.trim()); } - logger.info("Headers adapted {}", con.getHeaderFields()); - logger.info("Auth Response Code {} Message {} ", con.getResponseCode(), con.getResponseMessage()); - tokenFromUrl(con.getHeaderField(HttpHeader.LOCATION.toString())); + token.setMyBmwApiUsage(false); + return tokenFromUrl(con.getHeaderField(HttpHeader.LOCATION.toString())); } catch (IOException e) { logger.warn("{}", e.getMessage()); } + return false; } - void tokenFromUrl(String encodedUrl) { + public boolean tokenFromUrl(String encodedUrl) { + final StringBuilder result = new StringBuilder(); final MultiMap tokenMap = new MultiMap(); UrlEncoded.decodeTo(encodedUrl, tokenMap, StandardCharsets.US_ASCII); tokenMap.forEach((key, value) -> { @@ -420,6 +418,7 @@ void tokenFromUrl(String encodedUrl) { String val = value.get(0); if (key.endsWith(ACCESS_TOKEN)) { token.setToken(val.toString()); + result.append(true); } else if (key.equals(EXPIRES_IN)) { token.setExpiration(Integer.parseInt(val.toString())); } else if (key.equals(TOKEN_TYPE)) { @@ -427,16 +426,33 @@ void tokenFromUrl(String encodedUrl) { } } }); + return Boolean.valueOf(result.toString()); + } + + public static String codeFromUrl(String encodedUrl) { + final MultiMap tokenMap = new MultiMap(); + UrlEncoded.decodeTo(encodedUrl, tokenMap, StandardCharsets.US_ASCII); + final StringBuilder codeFound = new StringBuilder(); + tokenMap.forEach((key, value) -> { + if (value.size() > 0) { + String val = value.get(0); + if (key.endsWith(CODE)) { + codeFound.append(val.toString()); + } + } + }); + return codeFound.toString(); } private String getAuthEncodedData() { MultiMap dataMap = new MultiMap(); dataMap.add(CLIENT_ID, clientId); dataMap.add(RESPONSE_TYPE, TOKEN); - dataMap.add(REDIRECT_URI, BimmerConstants.REDIRECT_URI_VALUE); - dataMap.add(SCOPE, BimmerConstants.SCOPE_VALUES); + dataMap.add(REDIRECT_URI, BimmerConstants.LEGACY_REDIRECT_URI_VALUE); + dataMap.add(SCOPE, BimmerConstants.LEGACY_SCOPE_VALUES); dataMap.add(USERNAME, configuration.userName); dataMap.add(PASSWORD, configuration.password); - return UrlEncoded.encode(dataMap, StandardCharsets.UTF_8, false); + // return UrlEncoded.encode(dataMap, StandardCharsets.UTF_8, false); + return UrlEncoded.encode(dataMap, Charset.defaultCharset(), false); } } diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/RemoteServiceHandler.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/RemoteServiceHandler.java index fc1999d46aac0..86c619ccafc93 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/RemoteServiceHandler.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/RemoteServiceHandler.java @@ -13,7 +13,9 @@ package org.openhab.binding.bmwconnecteddrive.internal.handler; import static org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConstants.*; +import static org.openhab.binding.bmwconnecteddrive.internal.utils.HTTPConstants.*; +import java.nio.charset.StandardCharsets; import java.util.Optional; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -21,6 +23,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.util.MultiMap; +import org.eclipse.jetty.util.UrlEncoded; import org.openhab.binding.bmwconnecteddrive.internal.VehicleConfiguration; import org.openhab.binding.bmwconnecteddrive.internal.dto.NetworkError; import org.openhab.binding.bmwconnecteddrive.internal.dto.remote.ExecutionStatusContainer; @@ -45,6 +48,7 @@ public class RemoteServiceHandler implements StringResponseCallback { private final Logger logger = LoggerFactory.getLogger(RemoteServiceHandler.class); private static final String SERVICE_TYPE = "serviceType"; + private static final String EVENT_ID = "eventId"; private static final String DATA = "data"; // after 6 retries the state update will give up private static final int GIVEUP_COUNTER = 6; @@ -52,12 +56,16 @@ public class RemoteServiceHandler implements StringResponseCallback { private final ConnectedDriveProxy proxy; private final VehicleHandler handler; + private final String legacyServiceExecutionAPI; + private final String legacyServiceExecutionStateAPI; private final String serviceExecutionAPI; private final String serviceExecutionStateAPI; private int counter = 0; private Optional> stateJob = Optional.empty(); private Optional serviceExecuting = Optional.empty(); + private Optional executingEventId = Optional.empty(); + private boolean myBmwApiUsage = false; public enum ExecutionState { READY, @@ -76,7 +84,8 @@ public enum RemoteService { HORN_BLOW(REMOTE_SERVICE_HORN, "Horn Blow"), CLIMATE_NOW(REMOTE_SERVICE_AIR_CONDITIONING, "Climate Control"), CHARGE_NOW(REMOTE_SERVICE_CHARGE_NOW, "Start Charging"), - CHARGING_CONTROL(REMOTE_SERVICE_CHARGING_CONTROL, "Send Charging Profile"); + CHARGING_CONTROL(REMOTE_SERVICE_CHARGING_CONTROL, "Send Charging Profile"), + CHARGING_PROFILE("chargingProfile", "Send Charging Profile"); private final String command; private final String label; @@ -99,24 +108,39 @@ public RemoteServiceHandler(VehicleHandler vehicleHandler, ConnectedDriveProxy c handler = vehicleHandler; proxy = connectedDriveProxy; final VehicleConfiguration config = handler.getConfiguration().get(); - serviceExecutionAPI = proxy.baseUrl + config.vin + proxy.serviceExecutionAPI; - serviceExecutionStateAPI = proxy.baseUrl + config.vin + proxy.serviceExecutionStateAPI; + legacyServiceExecutionAPI = proxy.baseUrl + config.vin + proxy.serviceExecutionAPI; + legacyServiceExecutionStateAPI = proxy.baseUrl + config.vin + proxy.serviceExecutionStateAPI; + serviceExecutionAPI = proxy.remoteCommandUrl + config.vin + "/"; + serviceExecutionStateAPI = proxy.remoteStatusUrl; } boolean execute(RemoteService service, String... data) { synchronized (this) { if (serviceExecuting.isPresent()) { + logger.debug("Execution rejected - {} still pending", serviceExecuting.get()); // only one service executing return false; } serviceExecuting = Optional.of(service.name()); } - final MultiMap dataMap = new MultiMap(); - dataMap.add(SERVICE_TYPE, service.name()); - if (data.length > 0) { - dataMap.add(DATA, data[0]); + if (myBmwApiUsage) { + final MultiMap dataMap = new MultiMap(); + if (data.length > 0) { + dataMap.add(DATA, data[0]); + proxy.post(serviceExecutionAPI + service.name().toLowerCase().replace("_", "-"), + CONTENT_TYPE_JSON_ENCODED, "{CHARGING_PROFILE:" + data[0] + "}", this); + } else { + proxy.post(serviceExecutionAPI + service.name().toLowerCase().replace("_", "-"), null, null, this); + } + } else { + final MultiMap dataMap = new MultiMap(); + dataMap.add(SERVICE_TYPE, service.name()); + if (data.length > 0) { + dataMap.add(DATA, data[0]); + } + proxy.post(legacyServiceExecutionAPI, CONTENT_TYPE_URL_ENCODED, + UrlEncoded.encode(dataMap, StandardCharsets.UTF_8, false), this); } - proxy.post(serviceExecutionAPI, dataMap, this); return true; } @@ -130,9 +154,19 @@ public void getState() { handler.getData(); } counter++; - final MultiMap dataMap = new MultiMap(); - dataMap.add(SERVICE_TYPE, service); - proxy.get(serviceExecutionStateAPI, dataMap, this); + if (myBmwApiUsage) { + final MultiMap dataMap = new MultiMap(); + dataMap.add(EVENT_ID, executingEventId.get()); + final String encoded = dataMap == null || dataMap.isEmpty() ? null + : UrlEncoded.encode(dataMap, StandardCharsets.UTF_8, false); + + proxy.post(serviceExecutionStateAPI + Constants.QUESTION + encoded, null, null, this); + } else { + final MultiMap dataMap = new MultiMap(); + dataMap.add(SERVICE_TYPE, service); + proxy.get(legacyServiceExecutionStateAPI, CONTENT_TYPE_URL_ENCODED, + UrlEncoded.encode(dataMap, StandardCharsets.UTF_8, false), this); + } }, () -> { logger.warn("No Service executed to get state"); }); @@ -145,15 +179,36 @@ public void onResponse(@Nullable String result) { if (result != null) { try { ExecutionStatusContainer esc = Converter.getGson().fromJson(result, ExecutionStatusContainer.class); - if (esc != null && esc.executionStatus != null) { - String status = esc.executionStatus.status; - synchronized (this) { - handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null), status); - if (ExecutionState.EXECUTED.name().equals(status)) { - // refresh loop ends - update of status handled in the normal refreshInterval. Earlier - // update doesn't show better results! - reset(); - return; + if (esc != null) { + if (esc.executionStatus != null) { + // handling of BMW ConnectedDrive updates + String status = esc.executionStatus.status; + if (status != null) { + synchronized (this) { + handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null), status); + if (ExecutionState.EXECUTED.name().equals(status)) { + // refresh loop ends - update of status handled in the normal refreshInterval. + // Earlier + // update doesn't show better results! + reset(); + return; + } + } + } + } else if (esc.eventId != null) { + // store event id for further MyBMW updates + executingEventId = Optional.of(esc.eventId); + } else if (esc.eventStatus != null) { + // update status for MyBMW API + synchronized (this) { + handler.updateRemoteExecutionStatus(serviceExecuting.orElse(null), esc.eventStatus); + if (ExecutionState.EXECUTED.name().equals(esc.eventStatus)) { + // refresh loop ends - update of status handled in the normal refreshInterval. + // Earlier + // update doesn't show better results! + reset(); + return; + } } } } @@ -183,6 +238,7 @@ public void onError(NetworkError error) { private void reset() { serviceExecuting = Optional.empty(); + executingEventId = Optional.empty(); counter = 0; } @@ -196,4 +252,8 @@ public void cancel() { }); } } + + public void setMyBmwApiUsage(boolean b) { + myBmwApiUsage = b; + } } diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/Token.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/Token.java index 22e42170410d0..a1dd9cb452f86 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/Token.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/Token.java @@ -25,6 +25,15 @@ public class Token { private String token = Constants.EMPTY; private String tokenType = Constants.EMPTY; private long expiration = 0; + private boolean myBmwApiUsage = false; + + public boolean isMyBmwApiUsage() { + return myBmwApiUsage; + } + + public void setMyBmwApiUsage(boolean myBmwAppUsage) { + this.myBmwApiUsage = myBmwAppUsage; + } public String getBearerToken() { return new StringBuilder(tokenType).append(Constants.SPACE).append(token).toString(); @@ -52,4 +61,9 @@ public void setType(String type) { public boolean isValid() { return (!token.equals(Constants.EMPTY) && !tokenType.equals(Constants.EMPTY) && expiration > 0); } + + @Override + public String toString() { + return tokenType + token; + } } diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java index 40639e683e7f6..125e204098ef0 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java @@ -695,7 +695,6 @@ public void onResponse(@Nullable String content) { if (content != null) { try { NavigationContainer nav = Converter.getGson().fromJson(content, NavigationContainer.class); - updateChannel(CHANNEL_GROUP_RANGE, SOC_MAX, QuantityType.valueOf(nav.socmax, Units.KILOWATT_HOUR)); } catch (JsonSyntaxException jse) { logger.debug("{}", jse.getMessage()); diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/BimmerConstants.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/BimmerConstants.java index e3e9285107292..41ee3f057f45f 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/BimmerConstants.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/BimmerConstants.java @@ -34,41 +34,69 @@ public class BimmerConstants { public static final String REGION_ROW = "ROW"; // https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/country_selector.py - public static final String AUTH_SERVER_NORTH_AMERICA = "b2vapi.bmwgroup.us/gcdm"; - public static final String AUTH_SERVER_CHINA = "customer.bmwgroup.cn/gcdm"; - public static final String AUTH_SERVER_ROW = "customer.bmwgroup.com/gcdm"; - public static final Map AUTH_SERVER_MAP = Map.of(REGION_NORTH_AMERICA, AUTH_SERVER_NORTH_AMERICA, - REGION_CHINA, AUTH_SERVER_CHINA, REGION_ROW, AUTH_SERVER_ROW); - - // https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/country_selector.py - public static final String LEGACY_AUTH_SERVER_NORTH_AMERICA = "b2vapi.bmwgroup.us/gcdm"; - public static final String LEGACY_AUTH_SERVER_CHINA = "b2vapi.bmwgroup.cn/gcdm"; - public static final String LEGACY_AUTH_SERVER_ROW = "b2vapi.bmwgroup.com/gcdm"; + public static final String LEGACY_AUTH_SERVER_NORTH_AMERICA = "login.bmwusa.com/gcdm"; + public static final String LEGACY_AUTH_SERVER_CHINA = "customer.bmwgroup.cn/gcdm"; + public static final String LEGACY_AUTH_SERVER_ROW = "customer.bmwgroup.com/gcdm"; public static final Map LEGACY_AUTH_SERVER_MAP = Map.of(REGION_NORTH_AMERICA, LEGACY_AUTH_SERVER_NORTH_AMERICA, REGION_CHINA, LEGACY_AUTH_SERVER_CHINA, REGION_ROW, LEGACY_AUTH_SERVER_ROW); public static final String OAUTH_ENDPOINT = "/oauth/authenticate"; - public static final String LEGACY_OAUTH_ENDPOINT = "/oauth/token"; + public static final String TOKEN_ENDPOINT = "/oauth/token"; + + public static final String API_SERVER_NORTH_AMERICA = "b2vapi.bmwgroup.us"; + public static final String API_SERVER_CHINA = "b2vapi.bmwgroup.cn:8592"; + public static final String API_SERVER_ROW = "b2vapi.bmwgroup.com"; - public static final String SERVER_NORTH_AMERICA = "b2vapi.bmwgroup.us"; - public static final String SERVER_CHINA = "b2vapi.bmwgroup.cn:8592"; - public static final String SERVER_ROW = "b2vapi.bmwgroup.com"; - public static final Map SERVER_MAP = Map.of(REGION_NORTH_AMERICA, SERVER_NORTH_AMERICA, - REGION_CHINA, SERVER_CHINA, REGION_ROW, SERVER_ROW); + public static final String EADRAX_SERVER_NORTH_AMERICA = "cocoapi.bmwgroup.us"; + public static final String EADRAX_SERVER_ROW = "cocoapi.bmwgroup.com"; + public static final String EADRAX_SERVER_CHINA = Constants.EMPTY; + public static final Map EADRAX_SERVER_MAP = Map.of(REGION_NORTH_AMERICA, + EADRAX_SERVER_NORTH_AMERICA, REGION_CHINA, EADRAX_SERVER_CHINA, REGION_ROW, EADRAX_SERVER_ROW); + + public static final Map API_SERVER_MAP = Map.of(REGION_NORTH_AMERICA, API_SERVER_NORTH_AMERICA, + REGION_CHINA, API_SERVER_CHINA, REGION_ROW, API_SERVER_ROW); // see https://github.com/bimmerconnected/bimmer_connected/pull/252/files - public static final Map AUTHORIZATION_VALUE_MAP = Map.of(REGION_NORTH_AMERICA, + public static final Map LEGACY_AUTHORIZATION_VALUE_MAP = Map.of(REGION_NORTH_AMERICA, "Basic ZDc2NmI1MzctYTY1NC00Y2JkLWEzZGMtMGNhNTY3MmQ3ZjhkOjE1ZjY5N2Y2LWE1ZDUtNGNhZC05OWQ5LTNhMTViYzdmMzk3Mw==", REGION_CHINA, "Basic blF2NkNxdHhKdVhXUDc0eGYzQ0p3VUVQOjF6REh4NnVuNGNEanliTEVOTjNreWZ1bVgya0VZaWdXUGNRcGR2RFJwSUJrN3JPSg==", REGION_ROW, "Basic ZDc2NmI1MzctYTY1NC00Y2JkLWEzZGMtMGNhNTY3MmQ3ZjhkOjE1ZjY5N2Y2LWE1ZDUtNGNhZC05OWQ5LTNhMTViYzdmMzk3Mw=="); - public static final String CREDENTIAL_VALUES = "nQv6CqtxJuXWP74xf3CJwUEP:1zDHx6un4cDjybLENN3kyfumX2kEYigWPcQpdvDRpIBk7rOJ"; - public static final String REDIRECT_URI_VALUE = "https://www.bmw-connecteddrive.com/app/static/external-dispatch.html"; - public static final String SCOPE_VALUES = "authenticate_user vehicle_data remote_services"; - public static final String LEGACY_CREDENTIAL_VALUES = "nQv6CqtxJuXWP74xf3CJwUEP:1zDHx6un4cDjybLENN3kyfumX2kEYigWPcQpdvDRpIBk7rOJ"; - public static final String REFERER_URL = "https://www.bmw-connecteddrive.de/app/index.html"; + public static final String LEGACY_REDIRECT_URI_VALUE = "https://www.bmw-connecteddrive.com/app/static/external-dispatch.html"; + public static final String LEGACY_SCOPE_VALUES = "authenticate_user vehicle_data remote_services"; + + // public static final String LEGACY_CREDENTIAL_VALUES = + // "nQv6CqtxJuXWP74xf3CJwUEP:1zDHx6un4cDjybLENN3kyfumX2kEYigWPcQpdvDRpIBk7rOJ"; + public static final String LEGACY_REFERER_URL = "https://www.bmw-connecteddrive.de/app/index.html"; + + public static final String AUTH_SERVER_NORTH_AMERICA = "login.bmwusa.com/gcdm"; + public static final String AUTH_SERVER_CHINA = "customer.bmwgroup.cn/gcdm"; + public static final String AUTH_SERVER_ROW = "customer.bmwgroup.com/gcdm"; + public static final Map AUTH_SERVER_MAP = Map.of(REGION_NORTH_AMERICA, AUTH_SERVER_NORTH_AMERICA, + REGION_CHINA, AUTH_SERVER_CHINA, REGION_ROW, AUTH_SERVER_ROW); + + public static final Map AUTHORIZATION_VALUE_MAP = Map.of(REGION_NORTH_AMERICA, + "Basic NTQzOTRhNGItYjZjMS00NWZlLWI3YjItOGZkM2FhOTI1M2FhOmQ5MmYzMWMwLWY1NzktNDRmNS1hNzdkLTk2NmY4ZjAwZTM1MQ==", + REGION_CHINA, + "Basic blF2NkNxdHhKdVhXUDc0eGYzQ0p3VUVQOjF6REh4NnVuNGNEanliTEVOTjNreWZ1bVgya0VZaWdXUGNRcGR2RFJwSUJrN3JPSg==", + REGION_ROW, + "Basic MzFjMzU3YTAtN2ExZC00NTkwLWFhOTktMzNiOTcyNDRkMDQ4OmMwZTMzOTNkLTcwYTItNGY2Zi05ZDNjLTg1MzBhZjY0ZDU1Mg=="); + + public static final Map CODE_VERIFIER = Map.of(REGION_NORTH_AMERICA, + "BKDarcVUpgymBDCgHDH0PwwMfzycDxu1joeklioOhwXA", REGION_CHINA, Constants.EMPTY, REGION_ROW, + "7PsmfPS5MpaNt0jEcPpi-B7M7u0gs1Nzw6ex0Y9pa-0"); + + public static final Map CLIENT_ID = Map.of(REGION_NORTH_AMERICA, + "54394a4b-b6c1-45fe-b7b2-8fd3aa9253aa", REGION_CHINA, Constants.EMPTY, REGION_ROW, + "31c357a0-7a1d-4590-aa99-33b97244d048"); + + public static final Map STATE = Map.of(REGION_NORTH_AMERICA, "rgastJbZsMtup49-Lp0FMQ", REGION_CHINA, + Constants.EMPTY, REGION_ROW, "cEG9eLAIi6Nv-aaCAniziE_B6FPoobva3qr5gukilYw"); + + public static final String REDIRECT_URI_VALUE = "com.bmw.connected://oauth"; + public static final String SCOPE_VALUES = "openid profile email offline_access smacc vehicle_data perseus dlm svds cesim vsapi remote_services fupo authenticate_user"; } diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/HTTPConstants.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/HTTPConstants.java index 62f878691a238..8dd139fe0d102 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/HTTPConstants.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/HTTPConstants.java @@ -25,12 +25,15 @@ public class HTTPConstants { public static final String AUTH_HTTP_CLIENT_NAME = "AuthHttpClient"; public static final String CONTENT_TYPE_URL_ENCODED = "application/x-www-form-urlencoded"; - public static final String CONTENT_TYPE_JSON = "application/json"; + public static final String CONTENT_TYPE_JSON_ENCODED = "application/json"; public static final String KEEP_ALIVE = "Keep-Alive"; public static final String CLIENT_ID = "client_id"; public static final String RESPONSE_TYPE = "response_type"; public static final String TOKEN = "token"; + public static final String CODE = "code"; public static final String REDIRECT_URI = "redirect_uri"; + public static final String AUTHORIZATION = "authorization"; + public static final String GRANT_TYPE = "grant_type"; public static final String SCOPE = "scope"; public static final String CREDENTIALS = "Credentials"; public static final String USERNAME = "username"; diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/config/bridge-config.xml b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/config/bridge-config.xml index 66abc977fe925..3e61f843ea292 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/config/bridge-config.xml +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/config/bridge-config.xml @@ -24,5 +24,11 @@ ROW + + + Prefer *MyBMW* API instead of *BMW Connected Drive* + true + false + diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/i18n/bmwconnecteddrive_de.properties b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/i18n/bmwconnecteddrive_de.properties index b6306b789af7e..1c203a9d03403 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/i18n/bmwconnecteddrive_de.properties +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/resources/OH-INF/i18n/bmwconnecteddrive_de.properties @@ -5,12 +5,17 @@ binding.bmwconnecteddrive.description = Zeigt die Fahrzeugdaten # bridge types thing-type.bmwconnecteddrive.account.label = BMW ConnectedDrive Benutzerkonto thing-type.bmwconnecteddrive.account.description = Zugriff auf das BMW ConnectedDrive Portal f�r einen Benutzer -thing-type.config.bmwconnecteddrive.account.userName = Benutzername f�r das ConnectedDrive Portal -thing-type.config.bmwconnecteddrive.account.password = Passwort f�r das ConnectedDrive Portal -thing-type.config.bmwconnecteddrive.account.region = Auswahl Ihrer Region zur Verbindung mit dem korrekten BMW Server +thing-type.config.bmwconnecteddrive.account.userName.label = Benutzername +thing-type.config.bmwconnecteddrive.account.userName.description = Benutzername f�r das ConnectedDrive Portal +thing-type.config.bmwconnecteddrive.account.password.label = Passwort +thing-type.config.bmwconnecteddrive.account.password.description = Passwort f�r das ConnectedDrive Portal +thing-type.config.bmwconnecteddrive.account.region.label = Region +thing-type.config.bmwconnecteddrive.account.region.description = Auswahl Ihrer Region zur Verbindung mit dem korrekten BMW Server thing-type.config.bmwconnecteddrive.account.region.option.NORTH_AMERICA = Nordamerika thing-type.config.bmwconnecteddrive.account.region.option.CHINA = China thing-type.config.bmwconnecteddrive.account.region.option.ROW = Rest der Welt +thing-type.config.bmwconnecteddrive.account.preferMyBmw.label = Benutze MyBMW API +thing-type.config.bmwconnecteddrive.account.preferMyBmw.description = Benutzung des MyBMW API anstelle der BMW ConnectedDrive API # thing types thing-type.bmwconnecteddrive.bev_rex.label = Elektrofahrzeug mit REX diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AuthProbes.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AuthProbes.java index 4acdcdd3f1b02..c2f3b6895325d 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AuthProbes.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AuthProbes.java @@ -113,7 +113,7 @@ public AuthProbes(HttpClientFactory httpClientFactory, ConnectedDriveConfigurati // see https://customer.bmwgroup.com/one/app/oauth.js StringBuilder uri = new StringBuilder(); uri.append("https://customer.bmwgroup.com"); - if (BimmerConstants.SERVER_NORTH_AMERICA.equals(configuration.region)) { + if (BimmerConstants.LEGACY_AUTH_SERVER_MAP.equals(configuration.region)) { uri.append("/gcdm/usa/oauth/authenticate"); } else { uri.append("/gcdm/oauth/authenticate"); @@ -130,7 +130,7 @@ public AuthProbes(HttpClientFactory httpClientFactory, ConnectedDriveConfigurati } private String getRegionServer() { - String retVal = BimmerConstants.SERVER_MAP.get(configuration.region); + String retVal = BimmerConstants.LEGACY_AUTH_SERVER_MAP.get(configuration.region); if (retVal != null) { return retVal; } else { @@ -163,9 +163,9 @@ private synchronized void jettyUpdateToken() { req.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED); req.header(HttpHeader.CONNECTION, KEEP_ALIVE); - req.header(HttpHeader.HOST, BimmerConstants.SERVER_MAP.get(configuration.region)); + req.header(HttpHeader.HOST, BimmerConstants.LEGACY_AUTH_SERVER_MAP.get(configuration.region)); req.header(HttpHeader.AUTHORIZATION, BimmerConstants.AUTHORIZATION_VALUE_MAP.get(configuration.region)); - req.header(CREDENTIALS, BimmerConstants.CREDENTIAL_VALUES); + req.header(CREDENTIALS, BimmerConstants.LEGACY_CREDENTIAL_VALUES); MultiMap dataMap = new MultiMap(); dataMap.add(CLIENT_ID, clientId); @@ -208,10 +208,10 @@ public synchronized void updateToken() { con.setRequestProperty(HttpHeader.CONTENT_TYPE.toString(), CONTENT_TYPE_URL_ENCODED); con.setRequestProperty(HttpHeader.CONNECTION.toString(), KEEP_ALIVE); con.setRequestProperty(HttpHeader.HOST.toString(), - BimmerConstants.SERVER_MAP.get(BimmerConstants.SERVER_ROW)); + BimmerConstants.LEGACY_AUTH_SERVER_MAP.get(BimmerConstants.AUTH_SERVER_ROW)); con.setRequestProperty(HttpHeader.AUTHORIZATION.toString(), BimmerConstants.AUTHORIZATION_VALUE_MAP.get(configuration.region)); - con.setRequestProperty(CREDENTIALS, BimmerConstants.CREDENTIAL_VALUES); + con.setRequestProperty(CREDENTIALS, BimmerConstants.LEGACY_CREDENTIAL_VALUES); con.setDoOutput(true); MultiMap dataMap = new MultiMap(); @@ -249,7 +249,7 @@ private synchronized void legacyUpdateToken() { con.setRequestProperty(HttpHeader.CONTENT_TYPE.toString(), CONTENT_TYPE_URL_ENCODED); con.setRequestProperty(HttpHeader.CONNECTION.toString(), KEEP_ALIVE); con.setRequestProperty(HttpHeader.HOST.toString(), - BimmerConstants.SERVER_MAP.get(BimmerConstants.SERVER_ROW)); + BimmerConstants.LEGACY_AUTH_SERVER_MAP.get(BimmerConstants.LEGACY_AUTH_SERVER_ROW)); // con.setRequestProperty(HttpHeader.AUTHORIZATION.toString(), BimmerConstants.LEGACY_AUTHORIZATION_VALUE); con.setRequestProperty(CREDENTIALS, BimmerConstants.LEGACY_CREDENTIAL_VALUES); con.setDoOutput(true); diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AuthTest.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AuthTest.java index 8329c5f13e824..07efb6c6c8104 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AuthTest.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AuthTest.java @@ -23,17 +23,22 @@ import java.net.MalformedURLException; import java.net.ProtocolException; import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.util.Map; +import java.util.Optional; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.util.MultiMap; import org.eclipse.jetty.util.UrlEncoded; +import org.eclipse.jetty.util.ssl.SslContextFactory; import org.junit.jupiter.api.Test; import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConfiguration; +import org.openhab.binding.bmwconnecteddrive.internal.VehicleConfiguration; import org.openhab.binding.bmwconnecteddrive.internal.utils.BimmerConstants; +import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants; import org.openhab.binding.bmwconnecteddrive.internal.utils.HTTPConstants; import org.openhab.core.io.net.http.HttpClientFactory; import org.slf4j.Logger; @@ -47,15 +52,7 @@ */ @NonNullByDefault public class AuthTest { - private final Logger logger = LoggerFactory.getLogger(VehicleHandler.class); - - @Test - public void testAuthServerMap() { - Map authServers = BimmerConstants.AUTH_SERVER_MAP; - assertEquals(3, authServers.size(), "Number of Servers"); - Map api = BimmerConstants.SERVER_MAP; - assertEquals(3, api.size(), "Number of Servers"); - } + private final Logger logger = LoggerFactory.getLogger(AuthTest.class); @Test public void testTokenDecoding() { @@ -71,6 +68,7 @@ public void testTokenDecoding() { assertEquals("Bearer SfXKgkEXeeFJkVqdD4XMmfUU224MRuyh", t.getBearerToken(), "Token"); } + @Test public void testRealTokenUpdate() { ConnectedDriveConfiguration config = new ConnectedDriveConfiguration(); config.region = BimmerConstants.REGION_ROW; @@ -100,29 +98,24 @@ public void testJavaHttpAuth() { final MultiMap dataMap = new MultiMap(); dataMap.add("grant_type", "password"); - dataMap.add(SCOPE, BimmerConstants.SCOPE_VALUES); + dataMap.add(SCOPE, BimmerConstants.LEGACY_SCOPE_VALUES); dataMap.add(USERNAME, config.userName); dataMap.add(PASSWORD, config.password); String urlContent = UrlEncoded.encode(dataMap, StandardCharsets.UTF_8, false); - System.out.println(urlContent); url = new URL(legacyAuth.toString() + "?" + urlContent); - System.out.println(url.toString()); - System.out.println(Integer.toString(urlContent.length())); HttpURLConnection con = (HttpURLConnection) url.openConnection(); con.setRequestMethod("POST"); con.setRequestProperty(HttpHeader.CONTENT_LENGTH.asString(), Integer.toString(124)); con.setRequestProperty(HttpHeader.CONTENT_TYPE.asString(), "application/x-www-form-urlencoded"); // System.out.println(con.getHeaderField(HttpHeader.CONTENT_LENGTH.asString())); // con.setRequestProperty(HttpHeader.CONNECTION.asString(), KEEP_ALIVE); - con.setRequestProperty(HttpHeader.HOST.asString(), BimmerConstants.SERVER_MAP.get(config.region)); + con.setRequestProperty(HttpHeader.HOST.asString(), BimmerConstants.API_SERVER_MAP.get(config.region)); con.setRequestProperty(HttpHeader.AUTHORIZATION.asString(), - BimmerConstants.AUTHORIZATION_VALUE_MAP.get(config.region)); - con.setRequestProperty(CREDENTIALS, BimmerConstants.CREDENTIAL_VALUES); - con.setRequestProperty(HttpHeader.REFERER.asString(), BimmerConstants.REFERER_URL); - System.out.println(con.getHeaderFields()); + BimmerConstants.LEGACY_AUTHORIZATION_VALUE_MAP.get(config.region)); + con.setRequestProperty(CREDENTIALS, BimmerConstants.LEGACY_CREDENTIAL_VALUES); + con.setRequestProperty(HttpHeader.REFERER.asString(), BimmerConstants.LEGACY_REFERER_URL); int status = con.getResponseCode(); - System.out.println("Status: " + status); if (status < 400) { BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); String inputLine; @@ -130,10 +123,8 @@ public void testJavaHttpAuth() { while ((inputLine = in.readLine()) != null) { content.append(inputLine); } - System.out.println("Content: " + content.toString()); in.close(); } - System.out.println(con.getContentLength()); con.disconnect(); } catch (MalformedURLException e) { } catch (ProtocolException e) { @@ -141,7 +132,45 @@ public void testJavaHttpAuth() { } } - public void testCumstomerBMWAuthenticate() { + @Test + public void urlencodeTest() { + String expectedResult = "client_id=31c357a0-7a1d-4590-aa99-33b97244d048&response_type=code&redirect_uri=com.bmw.connected%3A%2F%2Foauth&state=cEG9eLAIi6Nv-aaCAniziE_B6FPoobva3qr5gukilYw&nonce=login_nonce&scope=openid+profile+email+offline_access+smacc+vehicle_data+perseus+dlm+svds+cesim+vsapi+remote_services+fupo+authenticate_user&authorization=G0s3x-LE682iWMJ3WSLm0TmB2R4.%2AAAJTSQACMDIAAlNLABw2c0llQVVyaTB5OFdpUlptQjVtWXhmaWNVTzQ9AAR0eXBlAANDVFMAAlMxAAIwMQ..%2A"; + + MultiMap baseValues = new MultiMap(); + baseValues.add(CLIENT_ID, Constants.EMPTY + BimmerConstants.CLIENT_ID.get("ROW")); + baseValues.add(RESPONSE_TYPE, CODE); + baseValues.add(REDIRECT_URI, BimmerConstants.REDIRECT_URI_VALUE); + baseValues.add("state", Constants.EMPTY + BimmerConstants.STATE.get("ROW")); + baseValues.add("nonce", "login_nonce"); + baseValues.add(SCOPE, BimmerConstants.SCOPE_VALUES); + + MultiMap codeChallenge = new MultiMap(); + codeChallenge.addAllValues(baseValues); + // codeChallenge.put("authorization", authCode); + logger.info("Code MultiMap {}", codeChallenge); + + // String codeEncoded = UrlEncoded.encode(codeChallenge, Charset.defaultCharset(), false); + String enc = URLEncoder.encode(codeChallenge.toString(), Charset.defaultCharset()); + logger.info("Code Url enc {}", enc); + + System.out.println(Charset.availableCharsets()); + String authCodeGot = "G0s3x-LE682iWMJ3WSLm0TmB2R4.*AAJTSQACMDIAAlNLABw2c0llQVVyaTB5OFdpUlptQjVtWXhmaWNVTzQ9AAR0eXBlAANDVFMAAlMxAAIwMQ..*"; + logger.info("Encoded {}", UrlEncoded.encodeString(authCodeGot)); + + codeChallenge.put("authorization", authCodeGot); + logger.info("Encoded {}", UrlEncoded.encode(codeChallenge, Charset.forName("UTF-8"), false)); + String authCodeSent = "G0s3x-LE682iWMJ3WSLm0TmB2R4.%2AAAJTSQACMDIAAlNLABw2c0llQVVyaTB5OFdpUlptQjVtWXhmaWNVTzQ9AAR0eXBlAANDVFMAAlMxAAIwMQ..%2A"; + } + + public void testLegacyJetty() { + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + HttpClient client = new HttpClient(sslContextFactory); + client.setIdleTimeout(20000); + try { + client.start(); + } catch (Exception e) { + logger.info("Client Start Exception {}", e.getMessage()); + } ConnectedDriveConfiguration c = new ConnectedDriveConfiguration(); c.region = "ROW"; c.userName = "bla"; @@ -151,7 +180,66 @@ public void testCumstomerBMWAuthenticate() { HttpClientFactory hct = mock(HttpClientFactory.class); when(hct.getCommonHttpClient()).thenReturn(hc); when(hct.createHttpClient(AUTH_HTTP_CLIENT_NAME)).thenReturn(hc); - AuthProbes auth = new AuthProbes(hct, c); - auth.updateToken(); + ConnectedDriveProxy auth = new ConnectedDriveProxy(hct, c); + auth.updateToken(client); + } + + @Test + public void testRemote() { + // String profile = + // "{\"weeklyPlanner\":{\"climatizationEnabled\":false,\"chargingMode\":\"IMMEDIATE_CHARGING\",\"chargingPreferences\":\"CHARGING_WINDOW\",\"timer1\":{\"departureTime\":\"16:00\",\"timerEnabled\":false,\"weekdays\":[\"MONDAY\",\"TUESDAY\",\"WEDNESDAY\",\"THURSDAY\",\"FRIDAY\",\"SATURDAY\",\"SUNDAY\"]},\"timer2\":{\"departureTime\":\"12:02\",\"timerEnabled\":false,\"weekdays\":[\"SATURDAY\"]},\"timer3\":{\"departureTime\":\"13:03\",\"timerEnabled\":false,\"weekdays\":[\"SATURDAY\"]},\"overrideTimer\":{\"departureTime\":\"00:00\",\"timerEnabled\":false,\"weekdays\":[]},\"preferredChargingWindow\":{\"startTime\":\"10:00\",\"endTime\":\"15:00\"}}}"; + VehicleConfiguration vc = new VehicleConfiguration(); + vc.vin = "WBY1Z81040V905639"; + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); + String profile = "{\"weeklyPlanner\":{\"climatizationEnabled\":false,\"chargingMode\":\"IMMEDIATE_CHARGING\",\"chargingPreferences\":\"CHARGING_WINDOW\",\"timer1\":{\"departureTime\":\"16:00\",\"timerEnabled\":false,\"weekdays\":[\"MONDAY\",\"TUESDAY\",\"WEDNESDAY\",\"THURSDAY\",\"FRIDAY\",\"SATURDAY\",\"SUNDAY\"]},\"timer2\":{\"departureTime\":\"12:02\",\"timerEnabled\":false,\"weekdays\":[\"SATURDAY\"]},\"timer3\":{\"departureTime\":\"13:03\",\"timerEnabled\":false,\"weekdays\":[\"SATURDAY\"]},\"overrideTimer\":{\"departureTime\":\"00:00\",\"timerEnabled\":false,\"weekdays\":[]},\"preferredChargingWindow\":{\"startTime\":\"10:00\",\"endTime\":\"15:00\"}}}"; + // String profile = + // "{\"type\":\"CHARGING_PROFILE\",\"weeklyPlanner\":{\"climatizationEnabled\":false,\"chargingMode\":\"IMMEDIATE_CHARGING\",\"chargingPreferences\":\"CHARGING_WINDOW\",\"timer1\":{\"departureTime\":\"16:00\",\"timerEnabled\":false,\"weekdays\":[\"MONDAY\",\"TUESDAY\",\"WEDNESDAY\",\"THURSDAY\",\"FRIDAY\",\"SATURDAY\",\"SUNDAY\"]},\"timer2\":{\"departureTime\":\"12:02\",\"timerEnabled\":false,\"weekdays\":[\"SATURDAY\"]},\"timer3\":{\"departureTime\":\"13:03\",\"timerEnabled\":false,\"weekdays\":[\"SATURDAY\"]},\"overrideTimer\":{\"departureTime\":\"00:00\",\"timerEnabled\":false,\"weekdays\":[]},\"preferredChargingWindow\":{\"startTime\":\"10:00\",\"endTime\":\"15:00\"}}}"; + String dataProfile = "{\"type\":\"CHARGING_PROFILE\",\"data\":" + profile + "}"; + HttpClient client = new HttpClient(sslContextFactory); + client.setIdleTimeout(20000); + try { + client.start(); + } catch (Exception e) { + logger.info("Client Start Exception {}", e.getMessage()); + } + ConnectedDriveConfiguration c = new ConnectedDriveConfiguration(); + c.region = "ROW"; + c.userName = "bla"; + c.password = "bla"; + + HttpClientFactory hct = mock(HttpClientFactory.class); + when(hct.getCommonHttpClient()).thenReturn(client); + when(hct.createHttpClient(AUTH_HTTP_CLIENT_NAME)).thenReturn(client); + VehicleHandler vh = mock(VehicleHandler.class); + when(vh.getConfiguration()).thenReturn(Optional.of(vc)); + ConnectedDriveProxy auth = new ConnectedDriveProxy(hct, c); + RemoteServiceHandler rsh = new RemoteServiceHandler(vh, auth); + rsh.setMyBmwApiUsage(true); + auth.updateToken(client); + // String url = "https://cocoapi.bmwgroup.com/eadrax-dcs/v1/send-to-car/send-to-car"; + String url = "https://cocoapi.bmwgroup.com" + ConnectedDriveProxy.REMOTE_SERVICE_EADRAX_BASE_URL + vc.vin + + "/vehicle-finder"; + // "/" + // + "charging-settings"; + // String url = "https://cocoapi.bmwgroup.com/eadrax-vrccs/v2/presentation/remote-history/" + vc.vin; + + String type = "{\"type\":\"LIGHTS\"}"; + // {"type":"CHARGING_PROFILE" + + auth.call(url, true, CONTENT_TYPE_JSON_ENCODED, dataProfile, rsh); + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + } + // MultiMap dataMap = new MultiMap<>(); + // dataMap.put("data", profile); + // auth.call(url, true, CONTENT_TYPE_URL_ENCODED, UrlEncoded.encode(dataMap, StandardCharsets.UTF_8, false), + // rsh); + // try { + // Thread.sleep(2000); + // } catch (InterruptedException e) { + // // TODO Auto-generated catch block + // e.printStackTrace(); + // } } } diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/auth_response.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/auth_response.json new file mode 100644 index 0000000000000..6d768df7251f0 --- /dev/null +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/auth_response.json @@ -0,0 +1,3 @@ +{ + "redirect_to": "redirect_uri=com.bmw.connected://oauth?client_id=31c357a0-7a1d-4590-aa99-33b97244d048&response_type=code&scope=openid profile email offline_access smacc vehicle_data perseus dlm svds cesim vsapi remote_services fupo authenticate_user&state=cEG9eLAIi6Nv-aaCAniziE_B6FPoobva3qr5gukilYw&authorization=XaTJvSCZePkXsQ3zLMbPyG2XpRo.*AAJTSQACMDIAAlNLABw2TmhkS25qQTQzc1lqUHdOYzNjanFZK1pkU2M9AAR0eXBlAANDVFMAAlMxAAIwMQ..*" +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/tokenResponse.json b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/tokenResponse.json new file mode 100644 index 0000000000000..b173c9e7c6f26 --- /dev/null +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/resources/responses/tokenResponse.json @@ -0,0 +1,8 @@ +{ + "token_type": "Bearer", + "access_token": "Iw-U6XS5zSeArLauaI-Ec6WFs88", + "refresh_token": "V3OAHd_foseD2nzTFV5_SsaMzGU", + "scope": "smacc vehicle_data perseus dlm svds openid profile vsapi remote_services authenticate_user cesim offline_access email fupo", + "expires_in": 3599, + "id_token": "eyJ0eXAiOiJKV1QiLCJraWQiOiIydGFUMUlOdTJFVE1QZFd4UWpIR3UyV3Q2T0E9IiwiYWxnIjoiUlMyNTYifQ.eyJhdF9oYXNoIjoiTGh1ZGZhT0pUOTBvYlNjYVhuN2RQUSIsInN1YiI6Im1hcmlrYS53ZXltYW5uQGdtYWlsLmNvbSIsImF1ZGl0VHJhY2tpbmdJZCI6ImJlNjcxM2M3LTY4NjgtNGU4My04NjIyLTg4ODMyNjg2MmU1OC0yODg4MDcwNDYiLCJnY2lkIjoiZDdjNTU5NjctNzQ5ZC00NjNiLTlhN2UtMTQ3ZGEwMmZiMzQ0IiwiaXNzIjoiaHR0cHM6Ly9jdXN0b21lci5ibXdncm91cC5jb20vYW0vb2F1dGgyIiwidG9rZW5OYW1lIjoiaWRfdG9rZW4iLCJhY3IiOiIwIiwiYXpwIjoiMzFjMzU3YTAtN2ExZC00NTkwLWFhOTktMzNiOTcyNDRkMDQ4IiwiYXV0aF90aW1lIjoxNjMwODYxOTE3LCJleHAiOjE2MzA4NjU1MTcsImlhdCI6MTYzMDg2MTkxNywiZW1haWwiOiJtYXJpa2Eud2V5bWFubkBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6Ik1BSUxfQUNUSVZFIiwiaG9tZV9tYXJrZXQiOiJERSIsImdpdmVuX25hbWUiOiJNYXJpa2EiLCJub25jZSI6ImxvZ2luX25vbmNlIiwiYXVkIjoiMzFjMzU3YTAtN2ExZC00NTkwLWFhOTktMzNiOTcyNDRkMDQ4IiwiY19oYXNoIjoiZVU5MjYyRTZiLUFlNzFfWHd2eWkwdyIsIm9yZy5mb3JnZXJvY2sub3BlbmlkY29ubmVjdC5vcHMiOiJwLWFnaGZMdlh1S29IcnNReTd1Z05xVEQyVEkiLCJzX2hhc2giOiJwcXpwa0pfS09mQ2htTTg4dFVLcExRIiwicmVhbG0iOiIvY3VzdG9tZXIiLCJzYWx1dGF0aW9uIjoiU0FMX01TIiwidG9rZW5UeXBlIjoiSldUVG9rZW4iLCJmYW1pbHlfbmFtZSI6IldleW1hbm4ifQ.LJxHE4BeUNh0YxhMIyF_LUa8hsAaGZ2VZot15vp_5SQWQvfGoC0KMgjuHawc-7CK01yDppR5awX2FwCsec3DemSUVvKeyjSg_of785dvCNsvcx9kvio-7nwet_6Acrv0bUlmpOtvN6GZpxE6NZi-ZkbEnw8KzrZvS8t6AgAv7dEeqPgVneZNu9XDSUM81QhS1X21FFGbyPD-9RnLt401Ft5WeKi4kN1ViCP7OkvpSOfRU3p4lv3fbsdoAoWU11Lp80TBYir8nJL-kykA076UK6qnks8zTFx1TlpPV0Nou5NgmqyLOprFaWk-9AG3gjhEYC2yLBMzQLHb8t2UYgAfUQ" +} \ No newline at end of file From 832753df3c34449b96eea599684a4c1466d9559e Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Sat, 18 Sep 2021 18:42:27 +0200 Subject: [PATCH 10/13] revert unwanted changes Signed-off-by: Bernd Weymann --- bundles/org.openhab.binding.ems/NOTICE | 13 - bundles/org.openhab.binding.ems/README.md | 56 - bundles/org.openhab.binding.ems/pom.xml | 17 - .../src/main/feature/feature.xml | 9 - .../binding/ems/dto/DailyForecast.java | 25 - .../binding/ems/dto/DayTemperature.java | 10 - .../openhab/binding/ems/dto/HourForecast.java | 21 - .../org/openhab/binding/ems/dto/HourRain.java | 8 - .../binding/ems/dto/OnecallWeather.java | 13 - .../binding/ems/dto/WeatherCurrent.java | 21 - .../binding/ems/dto/WeatherDescription.java | 8 - .../ems/internal/EMSBindingConstants.java | 34 - .../ems/internal/EMSConfiguration.java | 22 - .../ems/internal/EMSHandlerFactory.java | 65 - .../ems/internal/handler/EMSHandler.java | 159 -- .../ems/internal/handler/WeatherForecast.java | 95 - .../openhab/binding/ems/utils/Constants.java | 7 - .../openhab/binding/ems/utils/Formulas.java | 146 -- .../main/resources/OH-INF/binding/binding.xml | 9 - .../resources/OH-INF/i18n/ems_xx.properties | 21 - .../resources/OH-INF/thing/thing-types.xml | 58 - .../org/openhab/binding/ems/TestFormulas.java | 151 -- bundles/org.openhab.binding.ems/weather.json | 1787 ---------------- bundles/org.openhab.binding.ems/weather2.json | 1801 ----------------- bundles/org.openhab.binding.ems/weather3.json | 1777 ---------------- 25 files changed, 6333 deletions(-) delete mode 100644 bundles/org.openhab.binding.ems/NOTICE delete mode 100644 bundles/org.openhab.binding.ems/README.md delete mode 100644 bundles/org.openhab.binding.ems/pom.xml delete mode 100644 bundles/org.openhab.binding.ems/src/main/feature/feature.xml delete mode 100644 bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/DailyForecast.java delete mode 100644 bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/DayTemperature.java delete mode 100644 bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/HourForecast.java delete mode 100644 bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/HourRain.java delete mode 100644 bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/OnecallWeather.java delete mode 100644 bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/WeatherCurrent.java delete mode 100644 bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/WeatherDescription.java delete mode 100644 bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/EMSBindingConstants.java delete mode 100644 bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/EMSConfiguration.java delete mode 100644 bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/EMSHandlerFactory.java delete mode 100644 bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/handler/EMSHandler.java delete mode 100644 bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/handler/WeatherForecast.java delete mode 100644 bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/utils/Constants.java delete mode 100644 bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/utils/Formulas.java delete mode 100644 bundles/org.openhab.binding.ems/src/main/resources/OH-INF/binding/binding.xml delete mode 100644 bundles/org.openhab.binding.ems/src/main/resources/OH-INF/i18n/ems_xx.properties delete mode 100644 bundles/org.openhab.binding.ems/src/main/resources/OH-INF/thing/thing-types.xml delete mode 100644 bundles/org.openhab.binding.ems/src/test/java/org/openhab/binding/ems/TestFormulas.java delete mode 100644 bundles/org.openhab.binding.ems/weather.json delete mode 100644 bundles/org.openhab.binding.ems/weather2.json delete mode 100644 bundles/org.openhab.binding.ems/weather3.json diff --git a/bundles/org.openhab.binding.ems/NOTICE b/bundles/org.openhab.binding.ems/NOTICE deleted file mode 100644 index 38d625e349232..0000000000000 --- a/bundles/org.openhab.binding.ems/NOTICE +++ /dev/null @@ -1,13 +0,0 @@ -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 diff --git a/bundles/org.openhab.binding.ems/README.md b/bundles/org.openhab.binding.ems/README.md deleted file mode 100644 index 1a3861729841f..0000000000000 --- a/bundles/org.openhab.binding.ems/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# EMS Binding - -_Give some details about what this binding is meant for - a protocol, system, specific device._ - -_If possible, provide some resources like pictures, a video, etc. to give an impression of what can be done with this binding. You can place such resources into a `doc` folder next to this README.md._ - -## Supported Things - -_Please describe the different supported things / devices within this section._ -_Which different types are supported, which models were tested etc.?_ -_Note that it is planned to generate some part of this based on the XML files within ```src/main/resources/OH-INF/thing``` of your binding._ - -## Discovery - -_Describe the available auto-discovery features here. Mention for what it works and what needs to be kept in mind when using it._ - -## Binding Configuration - -_If your binding requires or supports general configuration settings, please create a folder ```cfg``` and place the configuration file ```.cfg``` inside it. In this section, you should link to this file and provide some information about the options. The file could e.g. look like:_ - -``` -# Configuration for the EMS Binding -# -# Default secret key for the pairing of the EMS Thing. -# It has to be between 10-40 (alphanumeric) characters. -# This may be changed by the user for security reasons. -secret=openHABSecret -``` - -_Note that it is planned to generate some part of this based on the information that is available within ```src/main/resources/OH-INF/binding``` of your binding._ - -_If your binding does not offer any generic configurations, you can remove this section completely._ - -## Thing Configuration - -_Describe what is needed to manually configure a thing, either through the UI or via a thing-file. This should be mainly about its mandatory and optional configuration parameters. A short example entry for a thing file can help!_ - -_Note that it is planned to generate some part of this based on the XML files within ```src/main/resources/OH-INF/thing``` of your binding._ - -## Channels - -_Here you should provide information about available channel types, what their meaning is and how they can be used._ - -_Note that it is planned to generate some part of this based on the XML files within ```src/main/resources/OH-INF/thing``` of your binding._ - -| channel | type | description | -|----------|--------|------------------------------| -| control | Switch | This is the control channel | - -## Full Example - -_Provide a full usage example based on textual configuration files (*.things, *.items, *.sitemap)._ - -## Any custom content here! - -_Feel free to add additional sections for whatever you think should also be mentioned about your binding!_ diff --git a/bundles/org.openhab.binding.ems/pom.xml b/bundles/org.openhab.binding.ems/pom.xml deleted file mode 100644 index 607ddbee7b035..0000000000000 --- a/bundles/org.openhab.binding.ems/pom.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - 4.0.0 - - - org.openhab.addons.bundles - org.openhab.addons.reactor.bundles - 3.1.0-SNAPSHOT - - - org.openhab.binding.ems - - openHAB Add-ons :: Bundles :: EMS Binding - - diff --git a/bundles/org.openhab.binding.ems/src/main/feature/feature.xml b/bundles/org.openhab.binding.ems/src/main/feature/feature.xml deleted file mode 100644 index b4dce3bcfb62f..0000000000000 --- a/bundles/org.openhab.binding.ems/src/main/feature/feature.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features - - - openhab-runtime-base - mvn:org.openhab.addons.bundles/org.openhab.binding.ems/${project.version} - - diff --git a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/DailyForecast.java b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/DailyForecast.java deleted file mode 100644 index f97537761e1e9..0000000000000 --- a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/DailyForecast.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.openhab.binding.ems.dto; - -import java.util.List; - -public class DailyForecast { - public int dt; // ":1620475200, - public int sunrise; // ":1620449957, - public int sunset; // ":1620504821, - public int moonrise; // ":1620447540, - public int moonset; // ":1620492060, - public double moon_phase; // ":0.9, - public DayTemperature temp; // ":{"day":284.62,"min":283.83,"max":285.33,"night":284.4,"eve":284.54,"morn":284.62}, - public DayTemperature feels_like; // ":{"day":284.07,"night":284.25,"eve":283.88,"morn":284.25}, - public int pressure; // ":993, - public int humidity; // ":86, - public double dew_point; // ":282.44, - public double wind_speed; // ":14.63, - public int wind_deg; // ":120, - public double wind_gust; // ":19.66, - public List weather; // ":[{"id":501,"main":"Rain","description":"moderate rain","icon":"10d"}], - public int clouds; // ":25, - public double pop; // ":1, - public double rain; // ":11.17, - public double uvi; // ":6.25 -} diff --git a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/DayTemperature.java b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/DayTemperature.java deleted file mode 100644 index 0febdd5920df2..0000000000000 --- a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/DayTemperature.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.openhab.binding.ems.dto; - -public class DayTemperature { - public double day; // ":284.62, - public double min; // ":283.83, - public double max; // ":285.33, - public double night; // ":284.4, - public double eve; // ":284.54, - public double morn; // ":284.62 -} diff --git a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/HourForecast.java b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/HourForecast.java deleted file mode 100644 index b8a6126d84f48..0000000000000 --- a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/HourForecast.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.openhab.binding.ems.dto; - -import java.util.List; - -public class HourForecast { - public int dt; // ":1620507600, - public double temp; // ":284.43, - public double feels_like; // ":283.73, - public int pressure; // ":987, - public int humidity; // ":81, - public double dew_point; // ":281.29, - public double uvi; // ":0, - public int clouds; // ":65, - public int visibility; // ":10000, - public double wind_speed; // ":13.78, - public int wind_deg; // ":190, - public double wind_gust; // ":17.82, - public List weather; // ":[{"id":500,"main":"Rain","description":"light rain","icon":"10n"}], - public double pop; // ":0.68, - public HourRain rain; // ":{"1h":0.23} -} diff --git a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/HourRain.java b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/HourRain.java deleted file mode 100644 index de467a0f408f6..0000000000000 --- a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/HourRain.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.openhab.binding.ems.dto; - -import com.google.gson.annotations.SerializedName; - -public class HourRain { - @SerializedName("1h") - public double oneh; // ":0.23 -} diff --git a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/OnecallWeather.java b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/OnecallWeather.java deleted file mode 100644 index 23d22c7e67f2c..0000000000000 --- a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/OnecallWeather.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.openhab.binding.ems.dto; - -import java.util.List; - -public class OnecallWeather { - public double lat; // ": 51.44, - public double lon; // ": -10, - public String timezone; // ": "Europe/Dublin", - public int timezone_offset; // ": 3600, - public WeatherCurrent current; - public List hourly; - public List daily; -} diff --git a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/WeatherCurrent.java b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/WeatherCurrent.java deleted file mode 100644 index 36c4088d680c2..0000000000000 --- a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/WeatherCurrent.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.openhab.binding.ems.dto; - -import java.util.List; - -public class WeatherCurrent { - public int dt; // ": 1620509255, - public int sunrisedt; // ": 1620449957, - public int sunsetdt; // ": 1620504821, - public double tempdt; // ": 284.43, - public double feels_likedt; // ": 283.73, - public int pressuredt; // ": 987, - public int humiditydt; // ": 81, - public double dew_pointdt; // ": 281.29, - public int uvidt; // ": 0, - public int cloudsdt; // ": 65, - public int visibilitydt; // ": 10000, - public double wind_speeddt; // ": 13.78, - public int wind_degdt; // ": 190, - public double wind_gustdt; // ": 17.82, - public List weather; -} diff --git a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/WeatherDescription.java b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/WeatherDescription.java deleted file mode 100644 index 5978a3c45358a..0000000000000 --- a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/dto/WeatherDescription.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.openhab.binding.ems.dto; - -public class WeatherDescription { - public int id; // ": 803, - public String main; // ": "Clouds", - public String description; // ": "broken clouds", - public String icon; // ": "04n" -} diff --git a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/EMSBindingConstants.java b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/EMSBindingConstants.java deleted file mode 100644 index 59486f28c6f2c..0000000000000 --- a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/EMSBindingConstants.java +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright (c) 2010-2021 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.ems.internal; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.core.thing.ThingTypeUID; - -/** - * The {@link EMSBindingConstants} class defines common constants, which are - * used across the whole binding. - * - * @author Bernd Weymann - Initial contribution - */ -@NonNullByDefault -public class EMSBindingConstants { - - private static final String BINDING_ID = "ems"; - - // List of all Thing Type UIDs - public static final ThingTypeUID THING_TYPE_EMS = new ThingTypeUID(BINDING_ID, "EMS"); - - // List of all Channel ids - public static final String CHANNEL_1 = "channel1"; -} diff --git a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/EMSConfiguration.java b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/EMSConfiguration.java deleted file mode 100644 index 7acd4352396ae..0000000000000 --- a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/EMSConfiguration.java +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) 2010-2021 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.ems.internal; - -/** - * The {@link EMSConfiguration} class contains fields mapping thing configuration parameters. - * - * @author Bernd Weymann - Initial contribution - */ -public class EMSConfiguration { - public String owmApiKey; -} diff --git a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/EMSHandlerFactory.java b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/EMSHandlerFactory.java deleted file mode 100644 index 886148e9df469..0000000000000 --- a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/EMSHandlerFactory.java +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright (c) 2010-2021 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.ems.internal; - -import static org.openhab.binding.ems.internal.EMSBindingConstants.THING_TYPE_EMS; - -import java.util.Set; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.ems.internal.handler.EMSHandler; -import org.openhab.core.i18n.LocationProvider; -import org.openhab.core.i18n.TimeZoneProvider; -import org.openhab.core.thing.Thing; -import org.openhab.core.thing.ThingTypeUID; -import org.openhab.core.thing.binding.BaseThingHandlerFactory; -import org.openhab.core.thing.binding.ThingHandler; -import org.openhab.core.thing.binding.ThingHandlerFactory; -import org.osgi.service.component.annotations.Component; -import org.osgi.service.component.annotations.Reference; - -/** - * The {@link EMSHandlerFactory} is responsible for creating things and thing - * handlers. - * - * @author Bernd Weymann - Initial contribution - */ -@NonNullByDefault -@Component(configurationPid = "binding.ems", service = ThingHandlerFactory.class) -public class EMSHandlerFactory extends BaseThingHandlerFactory { - - LocationProvider locationProvider; - - public EMSHandlerFactory(final @Reference LocationProvider lp, final @Reference TimeZoneProvider timeZoneProvider) { - locationProvider = lp; - } - - private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_EMS); - - @Override - public boolean supportsThingType(ThingTypeUID thingTypeUID) { - return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); - } - - @Override - protected @Nullable ThingHandler createHandler(Thing thing) { - ThingTypeUID thingTypeUID = thing.getThingTypeUID(); - - if (THING_TYPE_EMS.equals(thingTypeUID)) { - return new EMSHandler(thing, locationProvider.getLocation()); - } - - return null; - } -} diff --git a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/handler/EMSHandler.java b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/handler/EMSHandler.java deleted file mode 100644 index 884ee7b12683c..0000000000000 --- a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/handler/EMSHandler.java +++ /dev/null @@ -1,159 +0,0 @@ -/** - * Copyright (c) 2010-2021 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.ems.internal.handler; - -import static org.openhab.binding.ems.internal.EMSBindingConstants.CHANNEL_1; - -import java.util.Calendar; -import java.util.Optional; -import java.util.concurrent.TimeUnit; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.ems.internal.EMSConfiguration; -import org.openhab.binding.ems.utils.Formulas; -import org.openhab.core.library.types.PointType; -import org.openhab.core.thing.ChannelUID; -import org.openhab.core.thing.Thing; -import org.openhab.core.thing.ThingStatus; -import org.openhab.core.thing.binding.BaseThingHandler; -import org.openhab.core.types.Command; -import org.openhab.core.types.RefreshType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * The {@link EMSHandler} is responsible for handling commands, which are - * sent to one of the channels. - * - * @author Bernd Weymann - Initial contribution - */ -@NonNullByDefault -public class EMSHandler extends BaseThingHandler { - - private final Logger logger = LoggerFactory.getLogger(EMSHandler.class); - // Granularity of forecast in minutes - private final static int FORECAST_GRANULARITY_MIN = 5; - - private @Nullable EMSConfiguration config; - private Optional lastUpdate = Optional.empty(); - private Optional wf = Optional.empty(); - private double latitude = 0; - private double longitude = 0; - - public EMSHandler(Thing thing, @Nullable PointType pt) { - super(thing); - if (pt != null) { - latitude = pt.getLatitude().doubleValue(); - longitude = pt.getLongitude().doubleValue(); - } else { - logger.warn("No Location given - forecast will not work without havin location!"); - } - - } - - @Override - public void handleCommand(ChannelUID channelUID, Command command) { - if (CHANNEL_1.equals(channelUID.getId())) { - if (command instanceof RefreshType) { - // TODO: handle data refresh - } - - // TODO: handle command - - // Note: if communication with thing fails for some reason, - // indicate that by setting the status with detail information: - // updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - // "Could not control device at IP address x.x.x.x"); - } - } - - @Override - public void initialize() { - setConfiguration(getConfigAs(EMSConfiguration.class)); - updateStatus(ThingStatus.ONLINE); - wf = Optional.of(new WeatherForecast(latitude, longitude, config.owmApiKey)); - scheduler.scheduleAtFixedRate(this::update, 0, 5, TimeUnit.MINUTES); - } - - public void setConfiguration(EMSConfiguration c) { - config = c; - } - - public void update() { - Calendar update = Calendar.getInstance(); - if (newHour(lastUpdate, update)) { - - } - if (newDay(lastUpdate, update)) { - generatePrediction(); - } - lastUpdate = Optional.of(update); - } - - public void generatePrediction() { - wf = Optional.of(new WeatherForecast(latitude, longitude, config.owmApiKey)); - Calendar date = Calendar.getInstance(); - for (int days = 0; days < 7; days++) { - int observationDay = date.get(Calendar.DAY_OF_MONTH); - String observationDaate = date.get(Calendar.DAY_OF_MONTH) + "." + (date.get(Calendar.MONTH) + 1) + "." - + date.get(Calendar.YEAR); - logger.info("{}", observationDaate); - double dailyProduction = 0; - int dCounter = 0; - double dCloduiness = 0; - while (observationDay == date.get(Calendar.DAY_OF_MONTH)) { - int forecast_hour = date.get(Calendar.HOUR_OF_DAY); - int forecast_minute = date.get(Calendar.MINUTE); - double sunHeight = Formulas.round(Formulas.sunPositionDIN(date.get(Calendar.YEAR), - date.get(Calendar.MONTH) + 1, date.get(Calendar.DAY_OF_MONTH) + 1, forecast_hour, - forecast_minute * FORECAST_GRANULARITY_MIN, 0, latitude, 10, 2), 1); - if (sunHeight > 0) { - int cloudiness = wf.get().getCloudiness(observationDay, forecast_hour); - // logger.info("Cloudiness on {} {}: {}", observationDaate, forecast_hour, cloudiness); - double radiationInfo = Formulas.getRadiationInfo(Formulas.getCalendar(date.get(Calendar.YEAR), - date.get(Calendar.MONTH) + 1, date.get(Calendar.DAY_OF_MONTH) + 1, forecast_hour, - forecast_minute * FORECAST_GRANULARITY_MIN), sunHeight, latitude); - double adjustedRadiationInfo = radiationInfo * (100 - cloudiness) / 100; - double production = Formulas - .round(9.75 * adjustedRadiationInfo / 1000 * FORECAST_GRANULARITY_MIN / 60, 3); - double cloudProduction = Formulas.round(production * 100 - cloudiness / 100, 3); - if (cloudiness > 20 && cloudiness < 80) { - logger.info("Cloudiness: {} Factor {}", cloudiness, Double.valueOf(cloudiness) / 100.0); - } - // logger.info("Best Production: {} Cloud Production {}", production, - // Double.valueOf(cloudiness) / 100.0); - dailyProduction += production;// * (100 - cloudiness) / 100; - dCloduiness += cloudiness; - dCounter++; - } - date.add(Calendar.MINUTE, FORECAST_GRANULARITY_MIN); - } - // updateState(new ChannelUID("pv-prediction-today"), - // QuantityType.valueOf(dailyProduction, Units.KILOWATT_HOUR)); - dailyProduction = Formulas.round(dailyProduction, 0); - dCloduiness = Formulas.round(dCloduiness / dCounter, 0); - logger.info("Prediction for {} is {} with ~ {}% cloudiness", observationDaate, dailyProduction, - dCloduiness); - } - } - - private boolean newHour(Optional lastUpdate2, Calendar update) { - return lastUpdate2.isEmpty() ? true : lastUpdate2.get().get(Calendar.HOUR) != update.get(Calendar.HOUR); - } - - private boolean newDay(Optional lastUpdate2, Calendar update) { - return lastUpdate2.isEmpty() ? true - : lastUpdate2.get().get(Calendar.DAY_OF_YEAR) != update.get(Calendar.DAY_OF_YEAR); - } -} diff --git a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/handler/WeatherForecast.java b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/handler/WeatherForecast.java deleted file mode 100644 index ac7bfdfcf3cf4..0000000000000 --- a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/internal/handler/WeatherForecast.java +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Copyright (c) 2010-2021 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.ems.internal.handler; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.Calendar; -import java.util.Iterator; -import java.util.Optional; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.ems.dto.DailyForecast; -import org.openhab.binding.ems.dto.HourForecast; -import org.openhab.binding.ems.dto.OnecallWeather; -import org.openhab.binding.ems.utils.Constants; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * The {@link WeatherForecast} handles forecast data - * - * @author Bernd Weymann - Initial contribution - */ -@NonNullByDefault -public class WeatherForecast { - - private final Logger logger = LoggerFactory.getLogger(WeatherForecast.class); - private Optional ocw = Optional.empty(); - - public WeatherForecast(double lat, double lon, String apiKey) { - try { - URL weatherApi = new URL( - "https://api.openweathermap.org/data/2.5/onecall?lat=" + lat + "&lon=" + lon + "&appid=" + apiKey); - HttpURLConnection con = (HttpURLConnection) weatherApi.openConnection(); - con.setRequestMethod("GET"); - BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); - String inputLine; - StringBuffer content = new StringBuffer(); - while ((inputLine = in.readLine()) != null) { - content.append(inputLine); - } - in.close(); - System.out.println(content.toString()); - OnecallWeather check = Constants.GSON.fromJson(content.toString(), OnecallWeather.class); - if (check != null) { - ocw = Optional.of(check); - } - } catch (MalformedURLException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - - public int getCloudiness(int day, int hour) { - if (ocw.isPresent()) { - // check if hourly forecast is present - for (Iterator iterator = ocw.get().hourly.iterator(); iterator.hasNext();) { - HourForecast hf = (HourForecast) iterator.next(); - Calendar c = Calendar.getInstance(); - c.setTimeInMillis((long) hf.dt * 1000); - if (c.get(Calendar.DAY_OF_MONTH) == day && c.get(Calendar.HOUR_OF_DAY) == hour) { - return hf.clouds; - } - } - // else return daily - for (Iterator iterator = ocw.get().daily.iterator(); iterator.hasNext();) { - DailyForecast df = (DailyForecast) iterator.next(); - Calendar c = Calendar.getInstance(); - c.setTimeInMillis((long) df.dt * 1000); - if (c.get(Calendar.DAY_OF_MONTH) == day) { - return df.clouds; - } - } - } - // logger.info("No Cloudiness found for day {} hour {}", day, hour); - return -1; - } -} diff --git a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/utils/Constants.java b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/utils/Constants.java deleted file mode 100644 index 8734aa0bcc050..0000000000000 --- a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/utils/Constants.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.openhab.binding.ems.utils; - -import com.google.gson.Gson; - -public class Constants { - public static final Gson GSON = new Gson(); -} diff --git a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/utils/Formulas.java b/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/utils/Formulas.java deleted file mode 100644 index ed349b3182074..0000000000000 --- a/bundles/org.openhab.binding.ems/src/main/java/org/openhab/binding/ems/utils/Formulas.java +++ /dev/null @@ -1,146 +0,0 @@ -package org.openhab.binding.ems.utils; - -import java.util.Calendar; - -public class Formulas { - - private static final double SC = 1367; // Solar constant in W/m² - public static final double DEG2RAD = Math.PI / 180; - public static final double RAD2DEG = 180. / Math.PI; - - public static Calendar getCalendar(int year, int month, int day, int hour, int minute) { - Calendar c = Calendar.getInstance(); - c.set(year, month, day, hour, minute); - return c; - } - - /** - * https://www.volker-quaschning.de/datserv/sunpos/sunpos.js - * https://wiki.energie-m.de/Sonnenstandsberechnung - * (GMT=0, MEZ=1, MESZ(Sommerzeit)=2) - */ - public static double sunPositionDIN(int year, int month, int day, int hour, int min, int sec, double lat, - double lon, int timezone) { - double J, J2; - double Zgl, MOZ, WOZ, w; - double decl; - double sunaz, sunhi; - double asinGs; - double acosAs; - - J2 = 365; - if (year % 4 == 0) { - J2++; - } - J = dayOfYear(year, month, day); - MOZ = hour + 1.0 / 60 * min + 1.0 / 3600 * sec - timezone + 1; - MOZ = MOZ - 4 * (15 - lon) / 60; - J = J * 360 / J2 + MOZ / 24; - decl = 0.3948 - 23.2559 * Math.cos(rad(J + 9.1)) - 0.3915 * Math.cos(rad(2 * J + 5.4)) - - 0.1764 * Math.cos(rad(3 * J + 26.0)); - Zgl = 0.0066 + 7.3525 * Math.cos(rad(J + 85.9)) + 9.9359 * Math.cos(rad(2 * J + 108.9)) - + 0.3387 * Math.cos(rad(3 * J + 105.2)); - WOZ = MOZ + Zgl / 60; - w = (12 - WOZ) * 15; - asinGs = Math.cos(rad(w)) * Math.cos(rad(lat)) * Math.cos(rad(decl)) + Math.sin(rad(lat)) * Math.sin(rad(decl)); - if (asinGs > 1) { - asinGs = 1; - } - if (asinGs < -1) { - asinGs = -1; - } - sunhi = grad(Math.asin(asinGs)); - acosAs = (Math.sin(rad(sunhi)) * Math.sin(rad(lat)) - Math.sin(rad(decl))) - / (Math.cos(rad(sunhi)) * Math.cos(rad(lat))); - if (acosAs > 1) { - acosAs = 1; - } - if (acosAs < -1) { - acosAs = -1; - } - sunaz = grad(Math.acos(acosAs)); - if ((WOZ > 12) || (WOZ < 0)) { - sunaz = 180 + sunaz; - } else { - sunaz = 180 - sunaz; - } - ; - double azimuth = sunaz;// * 1000) / 1000; - double height = sunhi;// * 1000) / 1000; - return round(height, 3); - } - - public static int dayOfYear(int year, int month, int day) { - int x; - int[] monthday = new int[] { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 }; - - x = monthday[month - 1] + day; - if ((year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0)) && (month > 2)) { - x++; - } - return x; - } - - public static double rad(double grad) { - return (grad * Math.PI / 180); - } - - public static double grad(double rad) { - return (rad * 180 / Math.PI); - } - - public static double airMass(double height) { - if (height > 0) { - double airmass = 1000 / Math.sin(rad(height)) / 1000; - double zenithangle = Math.round((90 - height) * 1000) / 1000; - return round(airmass, 3); - } - return 0; - } - - public static double round(double value, int places) { - if (places < 0) { - throw new IllegalArgumentException(); - } - - long factor = (long) Math.pow(10, places); - long tmp = Math.round(value * factor); - return (double) tmp / factor; - } - - /** - * https://en.wikipedia.org/wiki/Air_mass_(solar_energy) - */ - public static double solarIntensity(double airMass) { - double si = 1.1 * 1367 * (Math.pow(0.7, Math.pow(airMass, 0.678))); - return round(si, 3); - // {\displaystyle I=1.1\times I_{\mathrm {o} }\times 0.7^{(AM^{0.678})}\,} - } - - /** - * Calculates sun radiation data. - */ - public static double getRadiationInfo(Calendar calendar, double elevation, Double altitude) { - double sinAlpha = Math.sin(DEG2RAD * elevation); - - int dayOfYear = calendar.get(Calendar.DAY_OF_YEAR); - int daysInYear = calendar.getActualMaximum(Calendar.DAY_OF_YEAR); - - // Direct Solar Radiation (in W/m²) at the atmosphere entry - // At sunrise/sunset - calculations limits are reached - double rOut = (elevation > 3) ? SC * (0.034 * Math.cos(DEG2RAD * (360 * dayOfYear / daysInYear)) + 1) : 0; - double altitudeRatio = (altitude != null) ? 1 / Math.pow((1 - (6.5 / 288) * (altitude / 1000.0)), 5.256) : 1; - double m = (Math.sqrt(1229 + Math.pow(614 * sinAlpha, 2)) - 614 * sinAlpha) * altitudeRatio; - - // Direct radiation after atmospheric layer - // 0.6 = Coefficient de transmissivité - double rDir = rOut * Math.pow(0.6, m) * sinAlpha; - - // Diffuse Radiation - double rDiff = rOut * (0.271 - 0.294 * Math.pow(0.6, m)) * sinAlpha; - double rTot = rDir + rDiff; - - return rTot; - } - -} diff --git a/bundles/org.openhab.binding.ems/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.ems/src/main/resources/OH-INF/binding/binding.xml deleted file mode 100644 index 6ac7b6aeeb658..0000000000000 --- a/bundles/org.openhab.binding.ems/src/main/resources/OH-INF/binding/binding.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - EMS Binding - This is the binding for EMS. - - diff --git a/bundles/org.openhab.binding.ems/src/main/resources/OH-INF/i18n/ems_xx.properties b/bundles/org.openhab.binding.ems/src/main/resources/OH-INF/i18n/ems_xx.properties deleted file mode 100644 index 85bb996658174..0000000000000 --- a/bundles/org.openhab.binding.ems/src/main/resources/OH-INF/i18n/ems_xx.properties +++ /dev/null @@ -1,21 +0,0 @@ -# FIXME: please substitute the xx with a proper locale, ie. de -# FIXME: please do not add the file to the repo if you add or change no content -# binding -binding.ems.name = -binding.ems.description = - -# thing types -thing-type.ems.sample.label = -thing-type.ems.sample.description = - -# thing type config description -thing-type.config.ems.sample.hostname.label = -thing-type.config.ems.sample.hostname.description = -thing-type.config.ems.sample.password.label = -thing-type.config.ems.sample.password.description = -thing-type.config.ems.sample.refreshInterval.label = -thing-type.config.ems.sample.refreshInterval.description = - -# channel types -channel-type.ems.sample-channel.label = -channel-type.ems.sample-channel.description = diff --git a/bundles/org.openhab.binding.ems/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.ems/src/main/resources/OH-INF/thing/thing-types.xml deleted file mode 100644 index ef560ab05d6e0..0000000000000 --- a/bundles/org.openhab.binding.ems/src/main/resources/OH-INF/thing/thing-types.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - Sample thing for EMS Binding - - - - - - - - - - - - - - - - - - - - - - - - - - - network-address - - Hostname or IP address of the device - - - password - - Passwort to access the device - - - - Interval the device is polled in sec. - - - - - - - Number:Temperature - - Sample channel for EMS Binding - - diff --git a/bundles/org.openhab.binding.ems/src/test/java/org/openhab/binding/ems/TestFormulas.java b/bundles/org.openhab.binding.ems/src/test/java/org/openhab/binding/ems/TestFormulas.java deleted file mode 100644 index 7b5fe726ea121..0000000000000 --- a/bundles/org.openhab.binding.ems/src/test/java/org/openhab/binding/ems/TestFormulas.java +++ /dev/null @@ -1,151 +0,0 @@ -package org.openhab.binding.ems; - -import static org.mockito.Mockito.*; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.time.Instant; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.Calendar; - -import org.junit.jupiter.api.Test; -import org.openhab.binding.ems.dto.OnecallWeather; -import org.openhab.binding.ems.internal.EMSConfiguration; -import org.openhab.binding.ems.internal.handler.EMSHandler; -import org.openhab.binding.ems.utils.Constants; -import org.openhab.binding.ems.utils.Formulas; -import org.openhab.core.library.types.PointType; -import org.openhab.core.thing.Thing; -import org.openhab.core.thing.ThingUID; - -class TestFormulas { - - @Test - void test() { - int minuteGRanularity = 5; - int month = 5; - double totalProduction = 0; - for (int i = 0; i < 24; i++) { - for (int j = 0; j * minuteGRanularity < 60; j++) { - double sunHeight = Formulas - .round(Formulas.sunPositionDIN(2021, month, 8, i, j * minuteGRanularity, 0, 50, 10, 2), 1); - double airMass = Formulas.round(Formulas.airMass(sunHeight), 4); - double intensity = Formulas.round(Formulas.solarIntensity(airMass), 1); - double winkel = 90 - sunHeight + 15; - double realIntensity = Math.sin(Math.toRadians(winkel)); - double production = Formulas - .round(9.75 - * Formulas.getRadiationInfo( - Formulas.getCalendar(2021, month, 8, i, j * minuteGRanularity), sunHeight, 50.0) - / 1000 * minuteGRanularity / 60, 3); - if (sunHeight > 0) { - totalProduction += production; - System.out.println("Hour: " + i + " Height: " + sunHeight + " Air Mass: " + airMass + " Intensity: " - + intensity + " Winkel: " + winkel + " Production: " + production); - System.out.println("Rad: " - + Formulas.getRadiationInfo(Formulas.getCalendar(2021, 5, 8, i, 30), sunHeight, 50.0)); - } - } - } - System.out.println(totalProduction); - } - - @Test - public void test2() { - Calendar date = Calendar.getInstance(); - int minuteGranularity = 5; - double totalProduction = 0; - for (int i = 0; i < 24; i++) { - for (int j = 0; j * minuteGranularity < 60; j++) { - double sunHeight = Formulas - .round(Formulas.sunPositionDIN(date.get(Calendar.YEAR), date.get(Calendar.MONTH) + 1, - date.get(Calendar.DAY_OF_MONTH) + 1, i, j * minuteGranularity, 0, 50, 10, 2), 1); - double production = Formulas - .round(9.75 - * Formulas.getRadiationInfo( - Formulas.getCalendar(date.get(Calendar.YEAR), date.get(Calendar.MONTH) + 1, - date.get(Calendar.DAY_OF_MONTH) + 1, i, j * minuteGranularity), - sunHeight, 50.0) - / 1000 * minuteGranularity / 60, 3); - if (sunHeight > 0) { - totalProduction += production; - } - } - } - System.out.println(totalProduction); - } - - @Test - public void testWeather() { - try { - URL weatherApi = new URL( - "https://api.openweathermap.org/data/2.5/onecall?lat=51.44&lon=-10.0&appid=7c82a05c28361abc8ab90b9f0faf18fa"); - // URL weatherApi = new URL( - // "https://api.openweathermap.org/data/2.5/weather?q=Wetzlar&appid=7c82a05c28361abc8ab90b9f0faf18fa"); - HttpURLConnection con = (HttpURLConnection) weatherApi.openConnection(); - con.setRequestMethod("GET"); - BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); - String inputLine; - StringBuffer content = new StringBuffer(); - while ((inputLine = in.readLine()) != null) { - content.append(inputLine); - } - in.close(); - System.out.println(content.toString()); - OnecallWeather ocw = Constants.GSON.fromJson(content.toString(), OnecallWeather.class); - System.out.println(ocw.hourly.size()); - System.out.println(ocw.daily.size()); - } catch (MalformedURLException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - - @Test - public void testTimeConversion() { - long ut1 = Instant.now().getEpochSecond(); - System.out.println(ut1); - - System.out.println(Instant.ofEpochMilli(ut1)); - java.util.Date time = new java.util.Date(ut1); - System.out.println(time); - - java.util.Date time2 = new java.util.Date(1620507600); - System.out.println(time2); - - long epoch = Instant.now().toEpochMilli(); - System.out.println(epoch); - epoch = (long) 1620507600 * 1000; - System.out.println(epoch); - - LocalDate ld = Instant.ofEpochMilli(epoch).atZone(ZoneId.systemDefault()).toLocalDate(); - System.out.println(ld); - - LocalDateTime ldt = Instant.ofEpochMilli(epoch).atZone(ZoneId.systemDefault()).toLocalDateTime(); - System.out.println(ldt); - - java.util.Date time3 = new java.util.Date((long) 1620633600 * 1000); - System.out.println(time3); - } - - @Test - public void testEMSHandler() { - PointType pt = PointType.valueOf("50.55,8.43"); - Thing thing = mock(Thing.class); - when(thing.getUID()).thenReturn(new ThingUID("testbinding", "test")); - EMSHandler ems = new EMSHandler(thing, pt); - EMSConfiguration c = new EMSConfiguration(); - c.owmApiKey = "7c82a05c28361abc8ab90b9f0faf18fa"; - ems.setConfiguration(c); - ems.generatePrediction(); - } -} diff --git a/bundles/org.openhab.binding.ems/weather.json b/bundles/org.openhab.binding.ems/weather.json deleted file mode 100644 index da62df1818f50..0000000000000 --- a/bundles/org.openhab.binding.ems/weather.json +++ /dev/null @@ -1,1787 +0,0 @@ -{ - "lat": 51.44, - "lon": -10, - "timezone": "Europe/Dublin", - "timezone_offset": 3600, - "current": { - "dt": 1620509255, - "sunrise": 1620449957, - "sunset": 1620504821, - "temp": 284.43, - "feels_like": 283.73, - "pressure": 987, - "humidity": 81, - "dew_point": 281.29, - "uvi": 0, - "clouds": 65, - "visibility": 10000, - "wind_speed": 13.78, - "wind_deg": 190, - "wind_gust": 17.82, - "weather": [ - { - "id": 803, - "main": "Clouds", - "description": "broken clouds", - "icon": "04n" - } - ] - }, - "minutely": [ - { - "dt": 1620509280, - "precipitation": 0 - }, - { - "dt": 1620509340, - "precipitation": 0 - }, - { - "dt": 1620509400, - "precipitation": 0 - }, - { - "dt": 1620509460, - "precipitation": 0 - }, - { - "dt": 1620509520, - "precipitation": 0 - }, - { - "dt": 1620509580, - "precipitation": 0 - }, - { - "dt": 1620509640, - "precipitation": 0 - }, - { - "dt": 1620509700, - "precipitation": 0 - }, - { - "dt": 1620509760, - "precipitation": 0 - }, - { - "dt": 1620509820, - "precipitation": 0 - }, - { - "dt": 1620509880, - "precipitation": 0 - }, - { - "dt": 1620509940, - "precipitation": 0 - }, - { - "dt": 1620510000, - "precipitation": 0 - }, - { - "dt": 1620510060, - "precipitation": 0 - }, - { - "dt": 1620510120, - "precipitation": 0 - }, - { - "dt": 1620510180, - "precipitation": 0 - }, - { - "dt": 1620510240, - "precipitation": 0 - }, - { - "dt": 1620510300, - "precipitation": 0 - }, - { - "dt": 1620510360, - "precipitation": 0 - }, - { - "dt": 1620510420, - "precipitation": 0 - }, - { - "dt": 1620510480, - "precipitation": 0 - }, - { - "dt": 1620510540, - "precipitation": 0 - }, - { - "dt": 1620510600, - "precipitation": 0 - }, - { - "dt": 1620510660, - "precipitation": 0 - }, - { - "dt": 1620510720, - "precipitation": 0 - }, - { - "dt": 1620510780, - "precipitation": 0 - }, - { - "dt": 1620510840, - "precipitation": 0 - }, - { - "dt": 1620510900, - "precipitation": 0 - }, - { - "dt": 1620510960, - "precipitation": 0 - }, - { - "dt": 1620511020, - "precipitation": 0 - }, - { - "dt": 1620511080, - "precipitation": 0 - }, - { - "dt": 1620511140, - "precipitation": 0 - }, - { - "dt": 1620511200, - "precipitation": 0 - }, - { - "dt": 1620511260, - "precipitation": 0 - }, - { - "dt": 1620511320, - "precipitation": 0 - }, - { - "dt": 1620511380, - "precipitation": 0 - }, - { - "dt": 1620511440, - "precipitation": 0 - }, - { - "dt": 1620511500, - "precipitation": 0 - }, - { - "dt": 1620511560, - "precipitation": 0 - }, - { - "dt": 1620511620, - "precipitation": 0 - }, - { - "dt": 1620511680, - "precipitation": 0 - }, - { - "dt": 1620511740, - "precipitation": 0 - }, - { - "dt": 1620511800, - "precipitation": 0 - }, - { - "dt": 1620511860, - "precipitation": 0 - }, - { - "dt": 1620511920, - "precipitation": 0 - }, - { - "dt": 1620511980, - "precipitation": 0 - }, - { - "dt": 1620512040, - "precipitation": 0 - }, - { - "dt": 1620512100, - "precipitation": 0 - }, - { - "dt": 1620512160, - "precipitation": 0 - }, - { - "dt": 1620512220, - "precipitation": 0 - }, - { - "dt": 1620512280, - "precipitation": 0 - }, - { - "dt": 1620512340, - "precipitation": 0 - }, - { - "dt": 1620512400, - "precipitation": 0 - }, - { - "dt": 1620512460, - "precipitation": 0 - }, - { - "dt": 1620512520, - "precipitation": 0 - }, - { - "dt": 1620512580, - "precipitation": 0 - }, - { - "dt": 1620512640, - "precipitation": 0 - }, - { - "dt": 1620512700, - "precipitation": 0 - }, - { - "dt": 1620512760, - "precipitation": 0 - }, - { - "dt": 1620512820, - "precipitation": 0 - }, - { - "dt": 1620512880, - "precipitation": 0 - } - ], - "hourly": [ - { - "dt": 1620507600, - "temp": 284.43, - "feels_like": 283.73, - "pressure": 987, - "humidity": 81, - "dew_point": 281.29, - "uvi": 0, - "clouds": 65, - "visibility": 10000, - "wind_speed": 13.78, - "wind_deg": 190, - "wind_gust": 17.82, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10n" - } - ], - "pop": 0.68, - "rain": { - "1h": 0.23 - } - }, - { - "dt": 1620511200, - "temp": 284.4, - "feels_like": 283.7, - "pressure": 987, - "humidity": 81, - "dew_point": 281.26, - "uvi": 0, - "clouds": 66, - "visibility": 10000, - "wind_speed": 14.25, - "wind_deg": 192, - "wind_gust": 18.2, - "weather": [ - { - "id": 803, - "main": "Clouds", - "description": "broken clouds", - "icon": "04n" - } - ], - "pop": 0.85 - }, - { - "dt": 1620514800, - "temp": 284.35, - "feels_like": 283.67, - "pressure": 987, - "humidity": 82, - "dew_point": 281.39, - "uvi": 0, - "clouds": 68, - "visibility": 10000, - "wind_speed": 14.18, - "wind_deg": 192, - "wind_gust": 18.18, - "weather": [ - { - "id": 803, - "main": "Clouds", - "description": "broken clouds", - "icon": "04n" - } - ], - "pop": 0.73 - }, - { - "dt": 1620518400, - "temp": 284.25, - "feels_like": 283.58, - "pressure": 986, - "humidity": 83, - "dew_point": 281.48, - "uvi": 0, - "clouds": 70, - "visibility": 10000, - "wind_speed": 14.23, - "wind_deg": 190, - "wind_gust": 18.13, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10n" - } - ], - "pop": 0.94, - "rain": { - "1h": 0.13 - } - }, - { - "dt": 1620522000, - "temp": 284.11, - "feels_like": 283.45, - "pressure": 985, - "humidity": 84, - "dew_point": 281.52, - "uvi": 0, - "clouds": 27, - "visibility": 10000, - "wind_speed": 14.14, - "wind_deg": 192, - "wind_gust": 18.12, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10n" - } - ], - "pop": 0.68, - "rain": { - "1h": 0.17 - } - }, - { - "dt": 1620525600, - "temp": 284, - "feels_like": 283.33, - "pressure": 984, - "humidity": 84, - "dew_point": 281.54, - "uvi": 0, - "clouds": 23, - "visibility": 10000, - "wind_speed": 14.29, - "wind_deg": 192, - "wind_gust": 18.23, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10n" - } - ], - "pop": 0.57, - "rain": { - "1h": 0.11 - } - }, - { - "dt": 1620529200, - "temp": 284, - "feels_like": 283.33, - "pressure": 983, - "humidity": 84, - "dew_point": 281.53, - "uvi": 0, - "clouds": 31, - "visibility": 10000, - "wind_speed": 14.22, - "wind_deg": 191, - "wind_gust": 18.1, - "weather": [ - { - "id": 802, - "main": "Clouds", - "description": "scattered clouds", - "icon": "03n" - } - ], - "pop": 0.55 - }, - { - "dt": 1620532800, - "temp": 283.99, - "feels_like": 283.32, - "pressure": 983, - "humidity": 84, - "dew_point": 281.63, - "uvi": 0, - "clouds": 32, - "visibility": 10000, - "wind_speed": 14.66, - "wind_deg": 189, - "wind_gust": 18.59, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10n" - } - ], - "pop": 0.59, - "rain": { - "1h": 0.11 - } - }, - { - "dt": 1620536400, - "temp": 283.94, - "feels_like": 283.32, - "pressure": 982, - "humidity": 86, - "dew_point": 281.83, - "uvi": 0, - "clouds": 43, - "visibility": 10000, - "wind_speed": 15.2, - "wind_deg": 189, - "wind_gust": 19.13, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.63, - "rain": { - "1h": 0.2 - } - }, - { - "dt": 1620540000, - "temp": 284.26, - "feels_like": 283.57, - "pressure": 982, - "humidity": 82, - "dew_point": 281.45, - "uvi": 0.11, - "clouds": 52, - "visibility": 10000, - "wind_speed": 15.65, - "wind_deg": 195, - "wind_gust": 18.8, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.59, - "rain": { - "1h": 0.17 - } - }, - { - "dt": 1620543600, - "temp": 284.17, - "feels_like": 283.6, - "pressure": 982, - "humidity": 87, - "dew_point": 282.15, - "uvi": 0.5, - "clouds": 93, - "visibility": 10000, - "wind_speed": 14.76, - "wind_deg": 193, - "wind_gust": 18.18, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.55 - }, - { - "dt": 1620547200, - "temp": 284.18, - "feels_like": 283.61, - "pressure": 982, - "humidity": 87, - "dew_point": 282.13, - "uvi": 1.18, - "clouds": 62, - "visibility": 10000, - "wind_speed": 14.41, - "wind_deg": 196, - "wind_gust": 17.95, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.59, - "rain": { - "1h": 0.16 - } - }, - { - "dt": 1620550800, - "temp": 284.23, - "feels_like": 283.64, - "pressure": 982, - "humidity": 86, - "dew_point": 282.03, - "uvi": 2.2, - "clouds": 49, - "visibility": 10000, - "wind_speed": 14.22, - "wind_deg": 200, - "wind_gust": 18.08, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.59, - "rain": { - "1h": 0.14 - } - }, - { - "dt": 1620554400, - "temp": 284.21, - "feels_like": 283.62, - "pressure": 983, - "humidity": 86, - "dew_point": 282.03, - "uvi": 3.55, - "clouds": 46, - "visibility": 10000, - "wind_speed": 14.77, - "wind_deg": 202, - "wind_gust": 18.58, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.59, - "rain": { - "1h": 0.2 - } - }, - { - "dt": 1620558000, - "temp": 284.23, - "feels_like": 283.64, - "pressure": 983, - "humidity": 86, - "dew_point": 282.03, - "uvi": 4.7, - "clouds": 44, - "visibility": 10000, - "wind_speed": 14.73, - "wind_deg": 204, - "wind_gust": 18.73, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.59, - "rain": { - "1h": 0.11 - } - }, - { - "dt": 1620561600, - "temp": 284.23, - "feels_like": 283.64, - "pressure": 983, - "humidity": 86, - "dew_point": 282.13, - "uvi": 5.43, - "clouds": 42, - "visibility": 10000, - "wind_speed": 14.36, - "wind_deg": 206, - "wind_gust": 18.14, - "weather": [ - { - "id": 802, - "main": "Clouds", - "description": "scattered clouds", - "icon": "03d" - } - ], - "pop": 0.59 - }, - { - "dt": 1620565200, - "temp": 284.28, - "feels_like": 283.72, - "pressure": 983, - "humidity": 87, - "dew_point": 282.3, - "uvi": 5.26, - "clouds": 52, - "visibility": 10000, - "wind_speed": 13.22, - "wind_deg": 210, - "wind_gust": 16.49, - "weather": [ - { - "id": 803, - "main": "Clouds", - "description": "broken clouds", - "icon": "04d" - } - ], - "pop": 0.43 - }, - { - "dt": 1620568800, - "temp": 284.26, - "feels_like": 283.7, - "pressure": 983, - "humidity": 87, - "dew_point": 282.33, - "uvi": 4.75, - "clouds": 49, - "visibility": 10000, - "wind_speed": 11.84, - "wind_deg": 213, - "wind_gust": 14.97, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.43, - "rain": { - "1h": 0.16 - } - }, - { - "dt": 1620572400, - "temp": 284.3, - "feels_like": 283.74, - "pressure": 984, - "humidity": 87, - "dew_point": 282.35, - "uvi": 3.76, - "clouds": 37, - "visibility": 10000, - "wind_speed": 11.1, - "wind_deg": 215, - "wind_gust": 14.18, - "weather": [ - { - "id": 802, - "main": "Clouds", - "description": "scattered clouds", - "icon": "03d" - } - ], - "pop": 0.43 - }, - { - "dt": 1620576000, - "temp": 284.27, - "feels_like": 283.76, - "pressure": 984, - "humidity": 89, - "dew_point": 282.53, - "uvi": 2.81, - "clouds": 32, - "visibility": 10000, - "wind_speed": 10.08, - "wind_deg": 217, - "wind_gust": 12.75, - "weather": [ - { - "id": 802, - "main": "Clouds", - "description": "scattered clouds", - "icon": "03d" - } - ], - "pop": 0.4 - }, - { - "dt": 1620579600, - "temp": 284.19, - "feels_like": 283.67, - "pressure": 984, - "humidity": 89, - "dew_point": 282.64, - "uvi": 1.6, - "clouds": 28, - "visibility": 10000, - "wind_speed": 9.24, - "wind_deg": 219, - "wind_gust": 11.54, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.4, - "rain": { - "1h": 0.11 - } - }, - { - "dt": 1620583200, - "temp": 284.16, - "feels_like": 283.64, - "pressure": 985, - "humidity": 89, - "dew_point": 282.49, - "uvi": 0.73, - "clouds": 26, - "visibility": 10000, - "wind_speed": 8.05, - "wind_deg": 230, - "wind_gust": 9.99, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.4, - "rain": { - "1h": 0.23 - } - }, - { - "dt": 1620586800, - "temp": 284.1, - "feels_like": 283.55, - "pressure": 985, - "humidity": 88, - "dew_point": 282.33, - "uvi": 0.24, - "clouds": 25, - "visibility": 10000, - "wind_speed": 7.81, - "wind_deg": 240, - "wind_gust": 10.13, - "weather": [ - { - "id": 802, - "main": "Clouds", - "description": "scattered clouds", - "icon": "03d" - } - ], - "pop": 0.55 - }, - { - "dt": 1620590400, - "temp": 283.93, - "feels_like": 283.36, - "pressure": 985, - "humidity": 88, - "dew_point": 282.23, - "uvi": 0, - "clouds": 24, - "visibility": 10000, - "wind_speed": 8.03, - "wind_deg": 248, - "wind_gust": 10.05, - "weather": [ - { - "id": 801, - "main": "Clouds", - "description": "few clouds", - "icon": "02d" - } - ], - "pop": 0.41 - }, - { - "dt": 1620594000, - "temp": 283.8, - "feels_like": 283.24, - "pressure": 986, - "humidity": 89, - "dew_point": 282.1, - "uvi": 0, - "clouds": 39, - "visibility": 10000, - "wind_speed": 8.15, - "wind_deg": 249, - "wind_gust": 10.17, - "weather": [ - { - "id": 802, - "main": "Clouds", - "description": "scattered clouds", - "icon": "03n" - } - ], - "pop": 0.36 - }, - { - "dt": 1620597600, - "temp": 283.68, - "feels_like": 283.14, - "pressure": 986, - "humidity": 90, - "dew_point": 282.1, - "uvi": 0, - "clouds": 55, - "visibility": 10000, - "wind_speed": 8.09, - "wind_deg": 251, - "wind_gust": 9.95, - "weather": [ - { - "id": 803, - "main": "Clouds", - "description": "broken clouds", - "icon": "04n" - } - ], - "pop": 0.32 - }, - { - "dt": 1620601200, - "temp": 283.54, - "feels_like": 282.96, - "pressure": 986, - "humidity": 89, - "dew_point": 281.98, - "uvi": 0, - "clouds": 64, - "visibility": 10000, - "wind_speed": 7.73, - "wind_deg": 253, - "wind_gust": 9.53, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10n" - } - ], - "pop": 0.29, - "rain": { - "1h": 0.11 - } - }, - { - "dt": 1620604800, - "temp": 283.61, - "feels_like": 283.04, - "pressure": 986, - "humidity": 89, - "dew_point": 281.95, - "uvi": 0, - "clouds": 69, - "visibility": 10000, - "wind_speed": 7.38, - "wind_deg": 254, - "wind_gust": 9.35, - "weather": [ - { - "id": 803, - "main": "Clouds", - "description": "broken clouds", - "icon": "04n" - } - ], - "pop": 0.32 - }, - { - "dt": 1620608400, - "temp": 283.59, - "feels_like": 282.99, - "pressure": 986, - "humidity": 88, - "dew_point": 281.89, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 7.44, - "wind_deg": 264, - "wind_gust": 8.93, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10n" - } - ], - "pop": 0.64, - "rain": { - "1h": 0.19 - } - }, - { - "dt": 1620612000, - "temp": 283.43, - "feels_like": 282.81, - "pressure": 987, - "humidity": 88, - "dew_point": 281.73, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 8.66, - "wind_deg": 281, - "wind_gust": 10.05, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10n" - } - ], - "pop": 0.93, - "rain": { - "1h": 0.58 - } - }, - { - "dt": 1620615600, - "temp": 283.49, - "feels_like": 282.85, - "pressure": 987, - "humidity": 87, - "dew_point": 281.5, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 9.63, - "wind_deg": 277, - "wind_gust": 11.1, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10n" - } - ], - "pop": 0.93, - "rain": { - "1h": 0.44 - } - }, - { - "dt": 1620619200, - "temp": 283.24, - "feels_like": 282.58, - "pressure": 988, - "humidity": 87, - "dew_point": 281.38, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 10.31, - "wind_deg": 279, - "wind_gust": 11.8, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10n" - } - ], - "pop": 0.93, - "rain": { - "1h": 0.2 - } - }, - { - "dt": 1620622800, - "temp": 283.06, - "feels_like": 279.12, - "pressure": 988, - "humidity": 85, - "dew_point": 280.72, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 10.61, - "wind_deg": 281, - "wind_gust": 12.11, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.93, - "rain": { - "1h": 0.38 - } - }, - { - "dt": 1620626400, - "temp": 283.03, - "feels_like": 279, - "pressure": 989, - "humidity": 81, - "dew_point": 280.03, - "uvi": 0.1, - "clouds": 100, - "visibility": 10000, - "wind_speed": 11.01, - "wind_deg": 275, - "wind_gust": 12.17, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.93, - "rain": { - "1h": 0.17 - } - }, - { - "dt": 1620630000, - "temp": 282.97, - "feels_like": 278.86, - "pressure": 990, - "humidity": 80, - "dew_point": 279.71, - "uvi": 0.31, - "clouds": 100, - "visibility": 10000, - "wind_speed": 11.32, - "wind_deg": 275, - "wind_gust": 12.54, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.88, - "rain": { - "1h": 0.11 - } - }, - { - "dt": 1620633600, - "temp": 282.82, - "feels_like": 278.44, - "pressure": 991, - "humidity": 79, - "dew_point": 279.39, - "uvi": 0.73, - "clouds": 98, - "visibility": 10000, - "wind_speed": 12.49, - "wind_deg": 275, - "wind_gust": 13.8, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 1, - "rain": { - "1h": 0.25 - } - }, - { - "dt": 1620637200, - "temp": 282.84, - "feels_like": 278.33, - "pressure": 992, - "humidity": 75, - "dew_point": 278.74, - "uvi": 1.35, - "clouds": 99, - "visibility": 10000, - "wind_speed": 13.27, - "wind_deg": 272, - "wind_gust": 14.52, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 1, - "rain": { - "1h": 0.23 - } - }, - { - "dt": 1620640800, - "temp": 282.64, - "feels_like": 278.1, - "pressure": 993, - "humidity": 73, - "dew_point": 278.15, - "uvi": 2.73, - "clouds": 99, - "visibility": 10000, - "wind_speed": 13.04, - "wind_deg": 267, - "wind_gust": 15.01, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.8 - }, - { - "dt": 1620644400, - "temp": 282.69, - "feels_like": 278.16, - "pressure": 993, - "humidity": 72, - "dew_point": 277.93, - "uvi": 3.6, - "clouds": 96, - "visibility": 10000, - "wind_speed": 13.07, - "wind_deg": 255, - "wind_gust": 14.91, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 1, - "rain": { - "1h": 0.22 - } - }, - { - "dt": 1620648000, - "temp": 282.96, - "feels_like": 278.64, - "pressure": 994, - "humidity": 70, - "dew_point": 277.93, - "uvi": 4.16, - "clouds": 94, - "visibility": 10000, - "wind_speed": 12.41, - "wind_deg": 245, - "wind_gust": 14.22, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.79 - }, - { - "dt": 1620651600, - "temp": 283.01, - "feels_like": 278.78, - "pressure": 994, - "humidity": 68, - "dew_point": 277.61, - "uvi": 4.5, - "clouds": 86, - "visibility": 10000, - "wind_speed": 12.05, - "wind_deg": 237, - "wind_gust": 13.78, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.39 - }, - { - "dt": 1620655200, - "temp": 282.93, - "feels_like": 278.56, - "pressure": 995, - "humidity": 68, - "dew_point": 277.5, - "uvi": 4.07, - "clouds": 85, - "visibility": 10000, - "wind_speed": 12.66, - "wind_deg": 234, - "wind_gust": 13.9, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.4 - }, - { - "dt": 1620658800, - "temp": 282.89, - "feels_like": 278.56, - "pressure": 995, - "humidity": 68, - "dew_point": 277.38, - "uvi": 3.23, - "clouds": 61, - "visibility": 10000, - "wind_speed": 12.36, - "wind_deg": 228, - "wind_gust": 13.73, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.4, - "rain": { - "1h": 0.11 - } - }, - { - "dt": 1620662400, - "temp": 282.73, - "feels_like": 278.33, - "pressure": 995, - "humidity": 69, - "dew_point": 277.56, - "uvi": 2.42, - "clouds": 58, - "visibility": 10000, - "wind_speed": 12.41, - "wind_deg": 221, - "wind_gust": 13.37, - "weather": [ - { - "id": 803, - "main": "Clouds", - "description": "broken clouds", - "icon": "04d" - } - ], - "pop": 0.4 - }, - { - "dt": 1620666000, - "temp": 282.76, - "feels_like": 278.44, - "pressure": 995, - "humidity": 69, - "dew_point": 277.59, - "uvi": 1.4, - "clouds": 48, - "visibility": 10000, - "wind_speed": 12.06, - "wind_deg": 217, - "wind_gust": 13.38, - "weather": [ - { - "id": 802, - "main": "Clouds", - "description": "scattered clouds", - "icon": "03d" - } - ], - "pop": 0.4 - }, - { - "dt": 1620669600, - "temp": 282.85, - "feels_like": 278.64, - "pressure": 996, - "humidity": 70, - "dew_point": 277.73, - "uvi": 0.65, - "clouds": 42, - "visibility": 10000, - "wind_speed": 11.64, - "wind_deg": 215, - "wind_gust": 13.1, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.44, - "rain": { - "1h": 0.17 - } - }, - { - "dt": 1620673200, - "temp": 282.87, - "feels_like": 278.71, - "pressure": 996, - "humidity": 70, - "dew_point": 277.81, - "uvi": 0.22, - "clouds": 32, - "visibility": 10000, - "wind_speed": 11.38, - "wind_deg": 212, - "wind_gust": 12.78, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.58, - "rain": { - "1h": 0.17 - } - }, - { - "dt": 1620676800, - "temp": 282.97, - "feels_like": 278.93, - "pressure": 996, - "humidity": 69, - "dew_point": 277.71, - "uvi": 0, - "clouds": 33, - "visibility": 10000, - "wind_speed": 10.92, - "wind_deg": 204, - "wind_gust": 12.2, - "weather": [ - { - "id": 802, - "main": "Clouds", - "description": "scattered clouds", - "icon": "03d" - } - ], - "pop": 0.47 - } - ], - "daily": [ - { - "dt": 1620475200, - "sunrise": 1620449957, - "sunset": 1620504821, - "moonrise": 1620447540, - "moonset": 1620492060, - "moon_phase": 0.9, - "temp": { - "day": 284.62, - "min": 283.83, - "max": 285.33, - "night": 284.4, - "eve": 284.54, - "morn": 284.62 - }, - "feels_like": { - "day": 284.07, - "night": 284.25, - "eve": 283.88, - "morn": 284.25 - }, - "pressure": 993, - "humidity": 86, - "dew_point": 282.44, - "wind_speed": 14.63, - "wind_deg": 120, - "wind_gust": 19.66, - "weather": [ - { - "id": 501, - "main": "Rain", - "description": "moderate rain", - "icon": "10d" - } - ], - "clouds": 25, - "pop": 1, - "rain": 11.17, - "uvi": 6.25 - }, - { - "dt": 1620561600, - "sunrise": 1620536258, - "sunset": 1620591317, - "moonrise": 1620534780, - "moonset": 1620582540, - "moon_phase": 0.93, - "temp": { - "day": 284.23, - "min": 283.68, - "max": 284.35, - "night": 283.68, - "eve": 284.16, - "morn": 284.26 - }, - "feels_like": { - "day": 283.64, - "night": 283.57, - "eve": 283.64, - "morn": 283.57 - }, - "pressure": 983, - "humidity": 86, - "dew_point": 282.13, - "wind_speed": 15.65, - "wind_deg": 195, - "wind_gust": 19.13, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "clouds": 42, - "pop": 0.94, - "rain": 2, - "uvi": 5.43 - }, - { - "dt": 1620648000, - "sunrise": 1620622560, - "sunset": 1620677812, - "moonrise": 1620622020, - "moonset": 1620673080, - "moon_phase": 0.96, - "temp": { - "day": 282.96, - "min": 282.64, - "max": 283.61, - "night": 282.92, - "eve": 282.85, - "morn": 283.03 - }, - "feels_like": { - "day": 278.64, - "night": 279, - "eve": 278.64, - "morn": 279 - }, - "pressure": 994, - "humidity": 70, - "dew_point": 277.93, - "wind_speed": 13.27, - "wind_deg": 272, - "wind_gust": 15.01, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "clouds": 94, - "pop": 1, - "rain": 3.53, - "uvi": 4.5 - }, - { - "dt": 1620734400, - "sunrise": 1620708863, - "sunset": 1620764306, - "moonrise": 1620709320, - "moonset": 1620763620, - "moon_phase": 0, - "temp": { - "day": 282.77, - "min": 281.89, - "max": 284.08, - "night": 284.08, - "eve": 283.35, - "morn": 282.57 - }, - "feels_like": { - "day": 278.82, - "night": 278.28, - "eve": 282.51, - "morn": 278.28 - }, - "pressure": 995, - "humidity": 87, - "dew_point": 280.87, - "wind_speed": 12.31, - "wind_deg": 132, - "wind_gust": 13.13, - "weather": [ - { - "id": 501, - "main": "Rain", - "description": "moderate rain", - "icon": "10d" - } - ], - "clouds": 100, - "pop": 1, - "rain": 4.63, - "uvi": 1.66 - }, - { - "dt": 1620820800, - "sunrise": 1620795169, - "sunset": 1620850800, - "moonrise": 1620796860, - "moonset": 1620854160, - "moon_phase": 0.02, - "temp": { - "day": 283.83, - "min": 282.8, - "max": 284.28, - "night": 284.01, - "eve": 284.28, - "morn": 282.8 - }, - "feels_like": { - "day": 282.89, - "night": 281.15, - "eve": 283.51, - "morn": 281.15 - }, - "pressure": 1007, - "humidity": 74, - "dew_point": 279.45, - "wind_speed": 6.7, - "wind_deg": 297, - "wind_gust": 7.73, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "clouds": 37, - "pop": 0.2, - "rain": 0.2, - "uvi": 5.08 - }, - { - "dt": 1620907200, - "sunrise": 1620881476, - "sunset": 1620937293, - "moonrise": 1620884640, - "moonset": 1620944640, - "moon_phase": 0.05, - "temp": { - "day": 283.55, - "min": 283.18, - "max": 283.55, - "night": 283.34, - "eve": 283.44, - "morn": 283.18 - }, - "feels_like": { - "day": 282.87, - "night": 282.33, - "eve": 282.82, - "morn": 282.33 - }, - "pressure": 1011, - "humidity": 85, - "dew_point": 281.2, - "wind_speed": 11.94, - "wind_deg": 316, - "wind_gust": 12.37, - "weather": [ - { - "id": 800, - "main": "Clear", - "description": "clear sky", - "icon": "01d" - } - ], - "clouds": 4, - "pop": 0.26, - "uvi": 4.96 - }, - { - "dt": 1620993600, - "sunrise": 1620967786, - "sunset": 1621023785, - "moonrise": 1620972900, - "moonset": 0, - "moon_phase": 0.08, - "temp": { - "day": 284.2, - "min": 283.18, - "max": 285.07, - "night": 284.52, - "eve": 284.9, - "morn": 283.18 - }, - "feels_like": { - "day": 283.61, - "night": 282.48, - "eve": 284.3, - "morn": 282.48 - }, - "pressure": 1013, - "humidity": 86, - "dew_point": 282.02, - "wind_speed": 8.24, - "wind_deg": 336, - "wind_gust": 9.69, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "clouds": 52, - "pop": 0.47, - "rain": 1.03, - "uvi": 5 - }, - { - "dt": 1621080000, - "sunrise": 1621054097, - "sunset": 1621110276, - "moonrise": 1621061640, - "moonset": 1621034760, - "moon_phase": 0.11, - "temp": { - "day": 284.13, - "min": 283.56, - "max": 284.13, - "night": 283.72, - "eve": 284.06, - "morn": 283.68 - }, - "feels_like": { - "day": 283.56, - "night": 282.98, - "eve": 283.53, - "morn": 282.98 - }, - "pressure": 1017, - "humidity": 87, - "dew_point": 282.23, - "wind_speed": 11.6, - "wind_deg": 149, - "wind_gust": 15.48, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "clouds": 100, - "pop": 0.85, - "rain": 0.84, - "uvi": 5 - } - ] -} diff --git a/bundles/org.openhab.binding.ems/weather2.json b/bundles/org.openhab.binding.ems/weather2.json deleted file mode 100644 index 4411e829daed8..0000000000000 --- a/bundles/org.openhab.binding.ems/weather2.json +++ /dev/null @@ -1,1801 +0,0 @@ -{ - "lat": 50.55, - "lon": 8.43, - "timezone": "Europe/Berlin", - "timezone_offset": 7200, - "current": { - "dt": 1620604620, - "sunrise": 1620618337, - "sunset": 1620673188, - "temp": 288.71, - "feels_like": 288.04, - "pressure": 1004, - "humidity": 66, - "dew_point": 282.39, - "uvi": 0, - "clouds": 96, - "visibility": 10000, - "wind_speed": 0.89, - "wind_deg": 138, - "wind_gust": 1.34, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04n" - } - ] - }, - "minutely": [ - { - "dt": 1620604620, - "precipitation": 0 - }, - { - "dt": 1620604680, - "precipitation": 0 - }, - { - "dt": 1620604740, - "precipitation": 0 - }, - { - "dt": 1620604800, - "precipitation": 0 - }, - { - "dt": 1620604860, - "precipitation": 0 - }, - { - "dt": 1620604920, - "precipitation": 0 - }, - { - "dt": 1620604980, - "precipitation": 0 - }, - { - "dt": 1620605040, - "precipitation": 0 - }, - { - "dt": 1620605100, - "precipitation": 0 - }, - { - "dt": 1620605160, - "precipitation": 0 - }, - { - "dt": 1620605220, - "precipitation": 0 - }, - { - "dt": 1620605280, - "precipitation": 0 - }, - { - "dt": 1620605340, - "precipitation": 0 - }, - { - "dt": 1620605400, - "precipitation": 0 - }, - { - "dt": 1620605460, - "precipitation": 0 - }, - { - "dt": 1620605520, - "precipitation": 0 - }, - { - "dt": 1620605580, - "precipitation": 0 - }, - { - "dt": 1620605640, - "precipitation": 0 - }, - { - "dt": 1620605700, - "precipitation": 0 - }, - { - "dt": 1620605760, - "precipitation": 0 - }, - { - "dt": 1620605820, - "precipitation": 0 - }, - { - "dt": 1620605880, - "precipitation": 0 - }, - { - "dt": 1620605940, - "precipitation": 0 - }, - { - "dt": 1620606000, - "precipitation": 0 - }, - { - "dt": 1620606060, - "precipitation": 0 - }, - { - "dt": 1620606120, - "precipitation": 0 - }, - { - "dt": 1620606180, - "precipitation": 0 - }, - { - "dt": 1620606240, - "precipitation": 0 - }, - { - "dt": 1620606300, - "precipitation": 0 - }, - { - "dt": 1620606360, - "precipitation": 0 - }, - { - "dt": 1620606420, - "precipitation": 0 - }, - { - "dt": 1620606480, - "precipitation": 0 - }, - { - "dt": 1620606540, - "precipitation": 0 - }, - { - "dt": 1620606600, - "precipitation": 0 - }, - { - "dt": 1620606660, - "precipitation": 0 - }, - { - "dt": 1620606720, - "precipitation": 0 - }, - { - "dt": 1620606780, - "precipitation": 0 - }, - { - "dt": 1620606840, - "precipitation": 0 - }, - { - "dt": 1620606900, - "precipitation": 0 - }, - { - "dt": 1620606960, - "precipitation": 0 - }, - { - "dt": 1620607020, - "precipitation": 0 - }, - { - "dt": 1620607080, - "precipitation": 0 - }, - { - "dt": 1620607140, - "precipitation": 0.107 - }, - { - "dt": 1620607200, - "precipitation": 0.115 - }, - { - "dt": 1620607260, - "precipitation": 0.1762 - }, - { - "dt": 1620607320, - "precipitation": 0.2374 - }, - { - "dt": 1620607380, - "precipitation": 0.2986 - }, - { - "dt": 1620607440, - "precipitation": 0.3598 - }, - { - "dt": 1620607500, - "precipitation": 0.421 - }, - { - "dt": 1620607560, - "precipitation": 0.421 - }, - { - "dt": 1620607620, - "precipitation": 0.421 - }, - { - "dt": 1620607680, - "precipitation": 0.421 - }, - { - "dt": 1620607740, - "precipitation": 0.421 - }, - { - "dt": 1620607800, - "precipitation": 0.421 - }, - { - "dt": 1620607860, - "precipitation": 0.3914 - }, - { - "dt": 1620607920, - "precipitation": 0.3618 - }, - { - "dt": 1620607980, - "precipitation": 0.3322 - }, - { - "dt": 1620608040, - "precipitation": 0.3026 - }, - { - "dt": 1620608100, - "precipitation": 0.273 - }, - { - "dt": 1620608160, - "precipitation": 0.2816 - }, - { - "dt": 1620608220, - "precipitation": 0.2902 - } - ], - "hourly": [ - { - "dt": 1620601200, - "temp": 288.64, - "feels_like": 288.1, - "pressure": 1004, - "humidity": 71, - "dew_point": 283.41, - "uvi": 0, - "clouds": 96, - "visibility": 10000, - "wind_speed": 1.09, - "wind_deg": 174, - "wind_gust": 1.43, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04n" - } - ], - "pop": 0.68 - }, - { - "dt": 1620604800, - "temp": 288.71, - "feels_like": 288.04, - "pressure": 1004, - "humidity": 66, - "dew_point": 282.39, - "uvi": 0, - "clouds": 96, - "visibility": 10000, - "wind_speed": 1.08, - "wind_deg": 197, - "wind_gust": 1.43, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04n" - } - ], - "pop": 0.65 - }, - { - "dt": 1620608400, - "temp": 288.49, - "feels_like": 287.93, - "pressure": 1004, - "humidity": 71, - "dew_point": 283.27, - "uvi": 0, - "clouds": 97, - "visibility": 10000, - "wind_speed": 0.81, - "wind_deg": 149, - "wind_gust": 0.96, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10n" - } - ], - "pop": 0.22, - "rain": { - "1h": 0.32 - } - }, - { - "dt": 1620612000, - "temp": 288.13, - "feels_like": 287.67, - "pressure": 1004, - "humidity": 76, - "dew_point": 283.94, - "uvi": 0, - "clouds": 98, - "visibility": 10000, - "wind_speed": 1.45, - "wind_deg": 133, - "wind_gust": 1.4, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10n" - } - ], - "pop": 0.2, - "rain": { - "1h": 0.75 - } - }, - { - "dt": 1620615600, - "temp": 287.63, - "feels_like": 287.27, - "pressure": 1004, - "humidity": 82, - "dew_point": 284.6, - "uvi": 0, - "clouds": 98, - "visibility": 10000, - "wind_speed": 1.05, - "wind_deg": 107, - "wind_gust": 1.17, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04n" - } - ], - "pop": 0 - }, - { - "dt": 1620619200, - "temp": 286.55, - "feels_like": 286.24, - "pressure": 1005, - "humidity": 88, - "dew_point": 284.61, - "uvi": 0, - "clouds": 96, - "visibility": 10000, - "wind_speed": 1.25, - "wind_deg": 208, - "wind_gust": 1.28, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.01 - }, - { - "dt": 1620622800, - "temp": 287.06, - "feels_like": 286.96, - "pressure": 1007, - "humidity": 94, - "dew_point": 285.55, - "uvi": 0.15, - "clouds": 97, - "visibility": 10000, - "wind_speed": 1.8, - "wind_deg": 206, - "wind_gust": 2.68, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.02 - }, - { - "dt": 1620626400, - "temp": 288.93, - "feels_like": 288.94, - "pressure": 1007, - "humidity": 91, - "dew_point": 286.94, - "uvi": 0.49, - "clouds": 97, - "visibility": 10000, - "wind_speed": 0.87, - "wind_deg": 160, - "wind_gust": 3.87, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.33, - "rain": { - "1h": 0.2 - } - }, - { - "dt": 1620630000, - "temp": 288.38, - "feels_like": 288.44, - "pressure": 1007, - "humidity": 95, - "dew_point": 286.95, - "uvi": 0.19, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.81, - "wind_deg": 261, - "wind_gust": 4.32, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.47, - "rain": { - "1h": 0.17 - } - }, - { - "dt": 1620633600, - "temp": 288.58, - "feels_like": 288.55, - "pressure": 1008, - "humidity": 91, - "dew_point": 286.47, - "uvi": 0.34, - "clouds": 100, - "visibility": 10000, - "wind_speed": 3.23, - "wind_deg": 249, - "wind_gust": 8.89, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.54 - }, - { - "dt": 1620637200, - "temp": 287.51, - "feels_like": 287.46, - "pressure": 1008, - "humidity": 94, - "dew_point": 285.99, - "uvi": 0.51, - "clouds": 100, - "visibility": 10000, - "wind_speed": 2.19, - "wind_deg": 206, - "wind_gust": 11.01, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.58, - "rain": { - "1h": 0.19 - } - }, - { - "dt": 1620640800, - "temp": 288.19, - "feels_like": 288.1, - "pressure": 1009, - "humidity": 90, - "dew_point": 286.09, - "uvi": 4.45, - "clouds": 100, - "visibility": 10000, - "wind_speed": 3.41, - "wind_deg": 211, - "wind_gust": 10.74, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.54 - }, - { - "dt": 1620644400, - "temp": 288.07, - "feels_like": 287.99, - "pressure": 1009, - "humidity": 91, - "dew_point": 286.07, - "uvi": 5.03, - "clouds": 100, - "visibility": 10000, - "wind_speed": 3.19, - "wind_deg": 227, - "wind_gust": 9.3, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.46 - }, - { - "dt": 1620648000, - "temp": 289.55, - "feels_like": 289.36, - "pressure": 1009, - "humidity": 81, - "dew_point": 285.73, - "uvi": 5.01, - "clouds": 100, - "visibility": 10000, - "wind_speed": 3.24, - "wind_deg": 227, - "wind_gust": 7.62, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.42 - }, - { - "dt": 1620651600, - "temp": 289.55, - "feels_like": 289.33, - "pressure": 1009, - "humidity": 80, - "dew_point": 285.45, - "uvi": 2.14, - "clouds": 100, - "visibility": 10000, - "wind_speed": 2.71, - "wind_deg": 215, - "wind_gust": 5.32, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.46, - "rain": { - "1h": 0.26 - } - }, - { - "dt": 1620655200, - "temp": 290, - "feels_like": 289.67, - "pressure": 1010, - "humidity": 74, - "dew_point": 284.77, - "uvi": 1.64, - "clouds": 100, - "visibility": 10000, - "wind_speed": 3.79, - "wind_deg": 218, - "wind_gust": 6.77, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.34 - }, - { - "dt": 1620658800, - "temp": 289.53, - "feels_like": 289.16, - "pressure": 1010, - "humidity": 74, - "dew_point": 284.36, - "uvi": 1.08, - "clouds": 100, - "visibility": 10000, - "wind_speed": 2.8, - "wind_deg": 204, - "wind_gust": 4.23, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.34 - }, - { - "dt": 1620662400, - "temp": 287.92, - "feels_like": 287.72, - "pressure": 1010, - "humidity": 87, - "dew_point": 285.27, - "uvi": 0.6, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.97, - "wind_deg": 200, - "wind_gust": 3.95, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.5, - "rain": { - "1h": 0.35 - } - }, - { - "dt": 1620666000, - "temp": 286.47, - "feels_like": 286.34, - "pressure": 1010, - "humidity": 95, - "dew_point": 285.09, - "uvi": 0.25, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.21, - "wind_deg": 199, - "wind_gust": 2.26, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.59, - "rain": { - "1h": 0.58 - } - }, - { - "dt": 1620669600, - "temp": 285.83, - "feels_like": 285.66, - "pressure": 1011, - "humidity": 96, - "dew_point": 284.67, - "uvi": 0.07, - "clouds": 100, - "visibility": 7020, - "wind_speed": 1.67, - "wind_deg": 183, - "wind_gust": 2.8, - "weather": [ - { - "id": 501, - "main": "Rain", - "description": "moderate rain", - "icon": "10d" - } - ], - "pop": 0.73, - "rain": { - "1h": 1.07 - } - }, - { - "dt": 1620673200, - "temp": 285.34, - "feels_like": 285.15, - "pressure": 1010, - "humidity": 97, - "dew_point": 284.28, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 0.43, - "wind_deg": 304, - "wind_gust": 0.9, - "weather": [ - { - "id": 501, - "main": "Rain", - "description": "moderate rain", - "icon": "10n" - } - ], - "pop": 1, - "rain": { - "1h": 2.03 - } - }, - { - "dt": 1620676800, - "temp": 285.12, - "feels_like": 284.93, - "pressure": 1011, - "humidity": 98, - "dew_point": 284.16, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 0.89, - "wind_deg": 299, - "wind_gust": 1.16, - "weather": [ - { - "id": 501, - "main": "Rain", - "description": "moderate rain", - "icon": "10n" - } - ], - "pop": 1, - "rain": { - "1h": 1.09 - } - }, - { - "dt": 1620680400, - "temp": 285.05, - "feels_like": 284.85, - "pressure": 1010, - "humidity": 98, - "dew_point": 284.18, - "uvi": 0, - "clouds": 100, - "visibility": 9686, - "wind_speed": 0.91, - "wind_deg": 165, - "wind_gust": 0.97, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10n" - } - ], - "pop": 1, - "rain": { - "1h": 0.81 - } - }, - { - "dt": 1620684000, - "temp": 284.9, - "feels_like": 284.72, - "pressure": 1010, - "humidity": 99, - "dew_point": 284.1, - "uvi": 0, - "clouds": 100, - "visibility": 8743, - "wind_speed": 0.14, - "wind_deg": 152, - "wind_gust": 1.22, - "weather": [ - { - "id": 501, - "main": "Rain", - "description": "moderate rain", - "icon": "10n" - } - ], - "pop": 1, - "rain": { - "1h": 1.12 - } - }, - { - "dt": 1620687600, - "temp": 284.9, - "feels_like": 284.72, - "pressure": 1010, - "humidity": 99, - "dew_point": 284.1, - "uvi": 0, - "clouds": 100, - "visibility": 6532, - "wind_speed": 0.99, - "wind_deg": 226, - "wind_gust": 1.77, - "weather": [ - { - "id": 501, - "main": "Rain", - "description": "moderate rain", - "icon": "10n" - } - ], - "pop": 1, - "rain": { - "1h": 3.25 - } - }, - { - "dt": 1620691200, - "temp": 284.84, - "feels_like": 284.65, - "pressure": 1010, - "humidity": 99, - "dew_point": 284.03, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 0.88, - "wind_deg": 249, - "wind_gust": 1.28, - "weather": [ - { - "id": 501, - "main": "Rain", - "description": "moderate rain", - "icon": "10n" - } - ], - "pop": 1, - "rain": { - "1h": 2.1 - } - }, - { - "dt": 1620694800, - "temp": 284.75, - "feels_like": 284.52, - "pressure": 1009, - "humidity": 98, - "dew_point": 283.92, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.15, - "wind_deg": 277, - "wind_gust": 2.28, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10n" - } - ], - "pop": 0.89, - "rain": { - "1h": 0.5 - } - }, - { - "dt": 1620698400, - "temp": 284.9, - "feels_like": 284.69, - "pressure": 1008, - "humidity": 98, - "dew_point": 284.07, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.28, - "wind_deg": 274, - "wind_gust": 3.79, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10n" - } - ], - "pop": 0.92, - "rain": { - "1h": 0.76 - } - }, - { - "dt": 1620702000, - "temp": 284.98, - "feels_like": 284.78, - "pressure": 1009, - "humidity": 98, - "dew_point": 284.12, - "uvi": 0, - "clouds": 100, - "visibility": 6157, - "wind_speed": 1.95, - "wind_deg": 242, - "wind_gust": 3.74, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10n" - } - ], - "pop": 0.97, - "rain": { - "1h": 0.88 - } - }, - { - "dt": 1620705600, - "temp": 284.95, - "feels_like": 284.74, - "pressure": 1009, - "humidity": 98, - "dew_point": 284, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.2, - "wind_deg": 289, - "wind_gust": 4.04, - "weather": [ - { - "id": 501, - "main": "Rain", - "description": "moderate rain", - "icon": "10d" - } - ], - "pop": 1, - "rain": { - "1h": 1.44 - } - }, - { - "dt": 1620709200, - "temp": 285, - "feels_like": 284.8, - "pressure": 1009, - "humidity": 98, - "dew_point": 284.12, - "uvi": 0.02, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.3, - "wind_deg": 293, - "wind_gust": 3.56, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 1, - "rain": { - "1h": 0.65 - } - }, - { - "dt": 1620712800, - "temp": 285.3, - "feels_like": 285.1, - "pressure": 1008, - "humidity": 97, - "dew_point": 284.22, - "uvi": 0.05, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.63, - "wind_deg": 284, - "wind_gust": 4.51, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 1, - "rain": { - "1h": 0.29 - } - }, - { - "dt": 1620716400, - "temp": 285.89, - "feels_like": 285.67, - "pressure": 1009, - "humidity": 94, - "dew_point": 284.42, - "uvi": 0.19, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.64, - "wind_deg": 277, - "wind_gust": 3.33, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.37, - "rain": { - "1h": 0.14 - } - }, - { - "dt": 1620720000, - "temp": 285.77, - "feels_like": 285.57, - "pressure": 1008, - "humidity": 95, - "dew_point": 284.44, - "uvi": 0.33, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.5, - "wind_deg": 314, - "wind_gust": 3.17, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.35, - "rain": { - "1h": 0.14 - } - }, - { - "dt": 1620723600, - "temp": 286.46, - "feels_like": 286.25, - "pressure": 1008, - "humidity": 92, - "dew_point": 284.63, - "uvi": 0.5, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.87, - "wind_deg": 34, - "wind_gust": 5.4, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.35 - }, - { - "dt": 1620727200, - "temp": 286.3, - "feels_like": 286.07, - "pressure": 1008, - "humidity": 92, - "dew_point": 284.47, - "uvi": 0.8, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.57, - "wind_deg": 41, - "wind_gust": 4.81, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.27 - }, - { - "dt": 1620730800, - "temp": 286.08, - "feels_like": 285.83, - "pressure": 1008, - "humidity": 92, - "dew_point": 284.32, - "uvi": 0.9, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.28, - "wind_deg": 46, - "wind_gust": 3.42, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.4, - "rain": { - "1h": 0.15 - } - }, - { - "dt": 1620734400, - "temp": 285.84, - "feels_like": 285.62, - "pressure": 1009, - "humidity": 94, - "dew_point": 284.32, - "uvi": 0.89, - "clouds": 100, - "visibility": 10000, - "wind_speed": 0.56, - "wind_deg": 66, - "wind_gust": 2.08, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.54, - "rain": { - "1h": 0.27 - } - }, - { - "dt": 1620738000, - "temp": 285.58, - "feels_like": 285.39, - "pressure": 1009, - "humidity": 96, - "dew_point": 284.32, - "uvi": 0.42, - "clouds": 100, - "visibility": 10000, - "wind_speed": 0.66, - "wind_deg": 356, - "wind_gust": 2.17, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.7, - "rain": { - "1h": 0.37 - } - }, - { - "dt": 1620741600, - "temp": 285.49, - "feels_like": 285.29, - "pressure": 1009, - "humidity": 96, - "dew_point": 284.29, - "uvi": 0.32, - "clouds": 100, - "visibility": 10000, - "wind_speed": 0.69, - "wind_deg": 333, - "wind_gust": 1.83, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.72, - "rain": { - "1h": 0.27 - } - }, - { - "dt": 1620745200, - "temp": 285.3, - "feels_like": 285.08, - "pressure": 1008, - "humidity": 96, - "dew_point": 284.09, - "uvi": 0.21, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.44, - "wind_deg": 26, - "wind_gust": 4.43, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.71 - }, - { - "dt": 1620748800, - "temp": 286.61, - "feels_like": 286.28, - "pressure": 1008, - "humidity": 87, - "dew_point": 283.96, - "uvi": 0.49, - "clouds": 100, - "visibility": 10000, - "wind_speed": 0.98, - "wind_deg": 23, - "wind_gust": 2.28, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.69 - }, - { - "dt": 1620752400, - "temp": 286.4, - "feels_like": 286.1, - "pressure": 1008, - "humidity": 89, - "dew_point": 284.06, - "uvi": 0.21, - "clouds": 100, - "visibility": 10000, - "wind_speed": 0.96, - "wind_deg": 44, - "wind_gust": 1.85, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.69 - }, - { - "dt": 1620756000, - "temp": 285.73, - "feels_like": 285.45, - "pressure": 1008, - "humidity": 92, - "dew_point": 283.98, - "uvi": 0.06, - "clouds": 100, - "visibility": 10000, - "wind_speed": 0.73, - "wind_deg": 7, - "wind_gust": 0.88, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.61 - }, - { - "dt": 1620759600, - "temp": 284.5, - "feels_like": 284.17, - "pressure": 1009, - "humidity": 95, - "dew_point": 283.12, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 0.06, - "wind_deg": 265, - "wind_gust": 0.39, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.07 - }, - { - "dt": 1620763200, - "temp": 284.59, - "feels_like": 284.27, - "pressure": 1009, - "humidity": 95, - "dew_point": 283.18, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 0.09, - "wind_deg": 319, - "wind_gust": 0.35, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04n" - } - ], - "pop": 0.04 - }, - { - "dt": 1620766800, - "temp": 284.38, - "feels_like": 284.04, - "pressure": 1009, - "humidity": 95, - "dew_point": 283.09, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 0.79, - "wind_deg": 279, - "wind_gust": 0.75, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04n" - } - ], - "pop": 0 - }, - { - "dt": 1620770400, - "temp": 284.37, - "feels_like": 284.05, - "pressure": 1008, - "humidity": 96, - "dew_point": 283.17, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.33, - "wind_deg": 277, - "wind_gust": 1.62, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04n" - } - ], - "pop": 0 - } - ], - "daily": [ - { - "dt": 1620644400, - "sunrise": 1620618337, - "sunset": 1620673188, - "moonrise": 1620617640, - "moonset": 1620668340, - "moon_phase": 0.96, - "temp": { - "day": 288.07, - "min": 285.05, - "max": 290, - "night": 285.05, - "eve": 286.47, - "morn": 287.06 - }, - "feels_like": { - "day": 287.99, - "night": 286.96, - "eve": 286.34, - "morn": 286.96 - }, - "pressure": 1009, - "humidity": 91, - "dew_point": 286.07, - "wind_speed": 3.79, - "wind_deg": 218, - "wind_gust": 11.01, - "weather": [ - { - "id": 501, - "main": "Rain", - "description": "moderate rain", - "icon": "10d" - } - ], - "clouds": 100, - "pop": 1, - "rain": 8.25, - "uvi": 5.03 - }, - { - "dt": 1620730800, - "sunrise": 1620704645, - "sunset": 1620759679, - "moonrise": 1620705000, - "moonset": 1620758820, - "moon_phase": 0, - "temp": { - "day": 286.08, - "min": 284.38, - "max": 286.61, - "night": 284.38, - "eve": 286.4, - "morn": 285 - }, - "feels_like": { - "day": 285.83, - "night": 284.8, - "eve": 286.1, - "morn": 284.8 - }, - "pressure": 1008, - "humidity": 92, - "dew_point": 284.32, - "wind_speed": 1.95, - "wind_deg": 242, - "wind_gust": 5.4, - "weather": [ - { - "id": 501, - "main": "Rain", - "description": "moderate rain", - "icon": "10d" - } - ], - "clouds": 100, - "pop": 1, - "rain": 12.33, - "uvi": 0.9 - }, - { - "dt": 1620817200, - "sunrise": 1620790954, - "sunset": 1620846169, - "moonrise": 1620792540, - "moonset": 1620849300, - "moon_phase": 0.02, - "temp": { - "day": 284.66, - "min": 283.18, - "max": 285.13, - "night": 283.18, - "eve": 284.2, - "morn": 284.05 - }, - "feels_like": { - "day": 284.32, - "night": 283.73, - "eve": 283.76, - "morn": 283.73 - }, - "pressure": 1009, - "humidity": 94, - "dew_point": 283.13, - "wind_speed": 4.8, - "wind_deg": 291, - "wind_gust": 8.85, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "clouds": 100, - "pop": 0.81, - "rain": 2, - "uvi": 1.82 - }, - { - "dt": 1620903600, - "sunrise": 1620877264, - "sunset": 1620932658, - "moonrise": 1620880380, - "moonset": 1620939720, - "moon_phase": 0.05, - "temp": { - "day": 283.74, - "min": 281.94, - "max": 284.22, - "night": 282.39, - "eve": 282.64, - "morn": 281.94 - }, - "feels_like": { - "day": 283.26, - "night": 280.92, - "eve": 282.64, - "morn": 280.92 - }, - "pressure": 1008, - "humidity": 92, - "dew_point": 281.84, - "wind_speed": 3.28, - "wind_deg": 303, - "wind_gust": 7.5, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "clouds": 100, - "pop": 0.81, - "rain": 3, - "uvi": 1.83 - }, - { - "dt": 1620990000, - "sunrise": 1620963577, - "sunset": 1621019147, - "moonrise": 1620968640, - "moonset": 0, - "moon_phase": 0.08, - "temp": { - "day": 290.79, - "min": 281.49, - "max": 291.23, - "night": 282.39, - "eve": 287.13, - "morn": 283.24 - }, - "feels_like": { - "day": 289.68, - "night": 282.73, - "eve": 286.33, - "morn": 282.73 - }, - "pressure": 1009, - "humidity": 41, - "dew_point": 276.98, - "wind_speed": 2.9, - "wind_deg": 281, - "wind_gust": 3.76, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "clouds": 100, - "pop": 0.37, - "rain": 0.19, - "uvi": 5.25 - }, - { - "dt": 1621076400, - "sunrise": 1621049892, - "sunset": 1621105635, - "moonrise": 1621057380, - "moonset": 1621029840, - "moon_phase": 0.11, - "temp": { - "day": 290.66, - "min": 279.77, - "max": 290.66, - "night": 282.33, - "eve": 287.03, - "morn": 283.92 - }, - "feels_like": { - "day": 289.88, - "night": 282.98, - "eve": 286.48, - "morn": 282.98 - }, - "pressure": 1013, - "humidity": 54, - "dew_point": 280.69, - "wind_speed": 2.06, - "wind_deg": 10, - "wind_gust": 5, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "clouds": 99, - "pop": 0.39, - "rain": 0.59, - "uvi": 6 - }, - { - "dt": 1621162800, - "sunrise": 1621136208, - "sunset": 1621192121, - "moonrise": 1621146780, - "moonset": 1621119600, - "moon_phase": 0.14, - "temp": { - "day": 282.67, - "min": 280.69, - "max": 287.24, - "night": 280.77, - "eve": 285.15, - "morn": 281.38 - }, - "feels_like": { - "day": 282.67, - "night": 281.38, - "eve": 284.55, - "morn": 281.38 - }, - "pressure": 1016, - "humidity": 90, - "dew_point": 280.6, - "wind_speed": 2.15, - "wind_deg": 69, - "wind_gust": 5.06, - "weather": [ - { - "id": 501, - "main": "Rain", - "description": "moderate rain", - "icon": "10d" - } - ], - "clouds": 100, - "pop": 0.95, - "rain": 6.74, - "uvi": 6 - }, - { - "dt": 1621249200, - "sunrise": 1621222527, - "sunset": 1621278607, - "moonrise": 1621236780, - "moonset": 1621208820, - "moon_phase": 0.17, - "temp": { - "day": 284.85, - "min": 280.45, - "max": 287.32, - "night": 281.18, - "eve": 285.22, - "morn": 281.86 - }, - "feels_like": { - "day": 284.5, - "night": 281.86, - "eve": 284.75, - "morn": 281.86 - }, - "pressure": 1011, - "humidity": 93, - "dew_point": 283.09, - "wind_speed": 4.11, - "wind_deg": 244, - "wind_gust": 7.75, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "clouds": 100, - "pop": 1, - "rain": 7.89, - "uvi": 6 - } - ], - "alerts": [ - { - "sender_name": "Deutscher Wetterdienst", - "event": "WINDBÖEN", - "start": 1620619200, - "end": 1620673200, - "description": "Es treten oberhalb 400 m Windböen mit Geschwindigkeiten bis 60 km/h (17m/s, 33kn, Bft 7) aus südwestlicher Richtung auf. In exponierten Lagen muss mit Sturmböen bis 70 km/h (20m/s, 38kn, Bft 8) gerechnet werden." - }, - { - "sender_name": "Deutscher Wetterdienst", - "event": "wind gusts", - "start": 1620612000, - "end": 1620662400, - "description": "There is a risk of wind gusts (level 1 of 4).\nMax. gusts: \u003c 60 km/h; Wind direction: south-west; Increased gusts: in exposed locations \u003c 70 km/h" - } - ] -} diff --git a/bundles/org.openhab.binding.ems/weather3.json b/bundles/org.openhab.binding.ems/weather3.json deleted file mode 100644 index 7fe9d530bceb5..0000000000000 --- a/bundles/org.openhab.binding.ems/weather3.json +++ /dev/null @@ -1,1777 +0,0 @@ -{ - "lat": 50.55, - "lon": 8.43, - "timezone": "Europe/Berlin", - "timezone_offset": 7200, - "current": { - "dt": 1620648491, - "sunrise": 1620618337, - "sunset": 1620673188, - "temp": 291.86, - "feels_like": 291.27, - "pressure": 1009, - "humidity": 57, - "dew_point": 283.18, - "uvi": 5.01, - "clouds": 99, - "visibility": 10000, - "wind_speed": 2.24, - "wind_deg": 227, - "wind_gust": 4.92, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "rain": { - "1h": 0.56 - } - }, - "minutely": [ - { - "dt": 1620648540, - "precipitation": 0.562 - }, - { - "dt": 1620648600, - "precipitation": 0.562 - }, - { - "dt": 1620648660, - "precipitation": 0.5994 - }, - { - "dt": 1620648720, - "precipitation": 0.6368 - }, - { - "dt": 1620648780, - "precipitation": 0.6742 - }, - { - "dt": 1620648840, - "precipitation": 0.7116 - }, - { - "dt": 1620648900, - "precipitation": 0.749 - }, - { - "dt": 1620648960, - "precipitation": 0.799 - }, - { - "dt": 1620649020, - "precipitation": 0.849 - }, - { - "dt": 1620649080, - "precipitation": 0.899 - }, - { - "dt": 1620649140, - "precipitation": 0.949 - }, - { - "dt": 1620649200, - "precipitation": 0.999 - }, - { - "dt": 1620649260, - "precipitation": 0.999 - }, - { - "dt": 1620649320, - "precipitation": 0.999 - }, - { - "dt": 1620649380, - "precipitation": 0.999 - }, - { - "dt": 1620649440, - "precipitation": 0.999 - }, - { - "dt": 1620649500, - "precipitation": 0.999 - }, - { - "dt": 1620649560, - "precipitation": 0.9288 - }, - { - "dt": 1620649620, - "precipitation": 0.8586 - }, - { - "dt": 1620649680, - "precipitation": 0.7884 - }, - { - "dt": 1620649740, - "precipitation": 0.7182 - }, - { - "dt": 1620649800, - "precipitation": 0.648 - }, - { - "dt": 1620649860, - "precipitation": 0.5914 - }, - { - "dt": 1620649920, - "precipitation": 0.5348 - }, - { - "dt": 1620649980, - "precipitation": 0.4782 - }, - { - "dt": 1620650040, - "precipitation": 0.4216 - }, - { - "dt": 1620650100, - "precipitation": 0.365 - }, - { - "dt": 1620650160, - "precipitation": 0.3762 - }, - { - "dt": 1620650220, - "precipitation": 0.3874 - }, - { - "dt": 1620650280, - "precipitation": 0.3986 - }, - { - "dt": 1620650340, - "precipitation": 0.4098 - }, - { - "dt": 1620650400, - "precipitation": 0.421 - }, - { - "dt": 1620650460, - "precipitation": 0.4664 - }, - { - "dt": 1620650520, - "precipitation": 0.5118 - }, - { - "dt": 1620650580, - "precipitation": 0.5572 - }, - { - "dt": 1620650640, - "precipitation": 0.6026 - }, - { - "dt": 1620650700, - "precipitation": 0.648 - }, - { - "dt": 1620650760, - "precipitation": 0.7182 - }, - { - "dt": 1620650820, - "precipitation": 0.7884 - }, - { - "dt": 1620650880, - "precipitation": 0.8586 - }, - { - "dt": 1620650940, - "precipitation": 0.9288 - }, - { - "dt": 1620651000, - "precipitation": 0.999 - }, - { - "dt": 1620651060, - "precipitation": 1.1068 - }, - { - "dt": 1620651120, - "precipitation": 1.2146 - }, - { - "dt": 1620651180, - "precipitation": 1.3224 - }, - { - "dt": 1620651240, - "precipitation": 1.4302 - }, - { - "dt": 1620651300, - "precipitation": 1.538 - }, - { - "dt": 1620651360, - "precipitation": 1.538 - }, - { - "dt": 1620651420, - "precipitation": 1.538 - }, - { - "dt": 1620651480, - "precipitation": 1.538 - }, - { - "dt": 1620651540, - "precipitation": 1.538 - }, - { - "dt": 1620651600, - "precipitation": 1.538 - }, - { - "dt": 1620651660, - "precipitation": 1.5856 - }, - { - "dt": 1620651720, - "precipitation": 1.6332 - }, - { - "dt": 1620651780, - "precipitation": 1.6808 - }, - { - "dt": 1620651840, - "precipitation": 1.7284 - }, - { - "dt": 1620651900, - "precipitation": 1.776 - }, - { - "dt": 1620651960, - "precipitation": 1.6206 - }, - { - "dt": 1620652020, - "precipitation": 1.4652 - }, - { - "dt": 1620652080, - "precipitation": 1.3098 - }, - { - "dt": 1620652140, - "precipitation": 1.1544 - } - ], - "hourly": [ - { - "dt": 1620648000, - "temp": 291.86, - "feels_like": 291.27, - "pressure": 1009, - "humidity": 57, - "dew_point": 283.18, - "uvi": 5.01, - "clouds": 99, - "visibility": 10000, - "wind_speed": 5.42, - "wind_deg": 210, - "wind_gust": 8.69, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.36 - }, - { - "dt": 1620651600, - "temp": 291.3, - "feels_like": 290.82, - "pressure": 1009, - "humidity": 63, - "dew_point": 284.15, - "uvi": 2.14, - "clouds": 99, - "visibility": 10000, - "wind_speed": 3.41, - "wind_deg": 215, - "wind_gust": 6.65, - "weather": [ - { - "id": 501, - "main": "Rain", - "description": "moderate rain", - "icon": "10d" - } - ], - "pop": 0.61, - "rain": { - "1h": 1.54 - } - }, - { - "dt": 1620655200, - "temp": 290.9, - "feels_like": 290.45, - "pressure": 1009, - "humidity": 66, - "dew_point": 284.47, - "uvi": 1.64, - "clouds": 99, - "visibility": 10000, - "wind_speed": 3.65, - "wind_deg": 211, - "wind_gust": 5.87, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.49, - "rain": { - "1h": 0.56 - } - }, - { - "dt": 1620658800, - "temp": 289.43, - "feels_like": 289.07, - "pressure": 1010, - "humidity": 75, - "dew_point": 285, - "uvi": 1.08, - "clouds": 100, - "visibility": 10000, - "wind_speed": 3.35, - "wind_deg": 222, - "wind_gust": 7.4, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.6, - "rain": { - "1h": 0.15 - } - }, - { - "dt": 1620662400, - "temp": 288.23, - "feels_like": 288.01, - "pressure": 1010, - "humidity": 85, - "dew_point": 285.73, - "uvi": 0.6, - "clouds": 100, - "visibility": 10000, - "wind_speed": 2.24, - "wind_deg": 261, - "wind_gust": 6.35, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.66, - "rain": { - "1h": 0.32 - } - }, - { - "dt": 1620666000, - "temp": 286.27, - "feels_like": 286.17, - "pressure": 1011, - "humidity": 97, - "dew_point": 285.17, - "uvi": 0.25, - "clouds": 100, - "visibility": 6789, - "wind_speed": 1.39, - "wind_deg": 175, - "wind_gust": 2.77, - "weather": [ - { - "id": 501, - "main": "Rain", - "description": "moderate rain", - "icon": "10d" - } - ], - "pop": 0.7, - "rain": { - "1h": 1.65 - } - }, - { - "dt": 1620669600, - "temp": 285.62, - "feels_like": 285.46, - "pressure": 1010, - "humidity": 97, - "dew_point": 284.52, - "uvi": 0.07, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.56, - "wind_deg": 257, - "wind_gust": 2.32, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.78, - "rain": { - "1h": 0.74 - } - }, - { - "dt": 1620673200, - "temp": 285.06, - "feels_like": 284.87, - "pressure": 1010, - "humidity": 98, - "dew_point": 284.03, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.52, - "wind_deg": 302, - "wind_gust": 2.96, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10n" - } - ], - "pop": 1, - "rain": { - "1h": 0.67 - } - }, - { - "dt": 1620676800, - "temp": 284.81, - "feels_like": 284.59, - "pressure": 1011, - "humidity": 98, - "dew_point": 283.84, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 0.48, - "wind_deg": 184, - "wind_gust": 1.1, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10n" - } - ], - "pop": 1, - "rain": { - "1h": 0.62 - } - }, - { - "dt": 1620680400, - "temp": 284.57, - "feels_like": 284.33, - "pressure": 1011, - "humidity": 98, - "dew_point": 283.7, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 0.65, - "wind_deg": 322, - "wind_gust": 0.97, - "weather": [ - { - "id": 501, - "main": "Rain", - "description": "moderate rain", - "icon": "10n" - } - ], - "pop": 1, - "rain": { - "1h": 1.38 - } - }, - { - "dt": 1620684000, - "temp": 284.37, - "feels_like": 284.11, - "pressure": 1011, - "humidity": 98, - "dew_point": 283.55, - "uvi": 0, - "clouds": 100, - "visibility": 7586, - "wind_speed": 0.71, - "wind_deg": 265, - "wind_gust": 0.93, - "weather": [ - { - "id": 501, - "main": "Rain", - "description": "moderate rain", - "icon": "10n" - } - ], - "pop": 1, - "rain": { - "1h": 1.86 - } - }, - { - "dt": 1620687600, - "temp": 284.22, - "feels_like": 283.94, - "pressure": 1010, - "humidity": 98, - "dew_point": 283.38, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.31, - "wind_deg": 318, - "wind_gust": 3.13, - "weather": [ - { - "id": 501, - "main": "Rain", - "description": "moderate rain", - "icon": "10n" - } - ], - "pop": 1, - "rain": { - "1h": 1.22 - } - }, - { - "dt": 1620691200, - "temp": 284.08, - "feels_like": 283.79, - "pressure": 1010, - "humidity": 98, - "dew_point": 283.22, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.63, - "wind_deg": 304, - "wind_gust": 4.65, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10n" - } - ], - "pop": 1, - "rain": { - "1h": 0.37 - } - }, - { - "dt": 1620694800, - "temp": 284.08, - "feels_like": 283.76, - "pressure": 1010, - "humidity": 97, - "dew_point": 283.02, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.57, - "wind_deg": 283, - "wind_gust": 4.07, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10n" - } - ], - "pop": 0.69, - "rain": { - "1h": 0.14 - } - }, - { - "dt": 1620698400, - "temp": 283.82, - "feels_like": 283.48, - "pressure": 1009, - "humidity": 97, - "dew_point": 282.71, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.41, - "wind_deg": 284, - "wind_gust": 2.83, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04n" - } - ], - "pop": 0.65 - }, - { - "dt": 1620702000, - "temp": 283.57, - "feels_like": 283.2, - "pressure": 1009, - "humidity": 97, - "dew_point": 282.6, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 2.09, - "wind_deg": 274, - "wind_gust": 4.93, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10n" - } - ], - "pop": 0.73, - "rain": { - "1h": 0.33 - } - }, - { - "dt": 1620705600, - "temp": 283.43, - "feels_like": 283.05, - "pressure": 1009, - "humidity": 97, - "dew_point": 282.3, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 2.1, - "wind_deg": 280, - "wind_gust": 5.98, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.69 - }, - { - "dt": 1620709200, - "temp": 283.53, - "feels_like": 283.16, - "pressure": 1010, - "humidity": 97, - "dew_point": 282.38, - "uvi": 0.02, - "clouds": 100, - "visibility": 10000, - "wind_speed": 2.1, - "wind_deg": 282, - "wind_gust": 6.5, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.62 - }, - { - "dt": 1620712800, - "temp": 283.79, - "feels_like": 283.42, - "pressure": 1009, - "humidity": 96, - "dew_point": 282.57, - "uvi": 0.05, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.89, - "wind_deg": 308, - "wind_gust": 6.85, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.62 - }, - { - "dt": 1620716400, - "temp": 285.28, - "feels_like": 284.9, - "pressure": 1009, - "humidity": 90, - "dew_point": 283.16, - "uvi": 0.19, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.83, - "wind_deg": 308, - "wind_gust": 4.07, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.02 - }, - { - "dt": 1620720000, - "temp": 285.72, - "feels_like": 285.36, - "pressure": 1009, - "humidity": 89, - "dew_point": 283.46, - "uvi": 0.33, - "clouds": 100, - "visibility": 10000, - "wind_speed": 2.58, - "wind_deg": 311, - "wind_gust": 4.39, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0 - }, - { - "dt": 1620723600, - "temp": 286.75, - "feels_like": 286.33, - "pressure": 1008, - "humidity": 83, - "dew_point": 283.42, - "uvi": 0.5, - "clouds": 100, - "visibility": 10000, - "wind_speed": 2.43, - "wind_deg": 20, - "wind_gust": 4.31, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0 - }, - { - "dt": 1620727200, - "temp": 287.93, - "feels_like": 287.5, - "pressure": 1009, - "humidity": 78, - "dew_point": 283.46, - "uvi": 0.8, - "clouds": 100, - "visibility": 10000, - "wind_speed": 0.58, - "wind_deg": 357, - "wind_gust": 2.2, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0 - }, - { - "dt": 1620730800, - "temp": 286.49, - "feels_like": 286.2, - "pressure": 1009, - "humidity": 89, - "dew_point": 284.16, - "uvi": 0.9, - "clouds": 100, - "visibility": 10000, - "wind_speed": 0.62, - "wind_deg": 296, - "wind_gust": 1.67, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.2, - "rain": { - "1h": 0.29 - } - }, - { - "dt": 1620734400, - "temp": 285.98, - "feels_like": 285.77, - "pressure": 1009, - "humidity": 94, - "dew_point": 284.37, - "uvi": 0.89, - "clouds": 100, - "visibility": 10000, - "wind_speed": 0.66, - "wind_deg": 333, - "wind_gust": 1.59, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.39, - "rain": { - "1h": 0.53 - } - }, - { - "dt": 1620738000, - "temp": 285.83, - "feels_like": 285.61, - "pressure": 1009, - "humidity": 94, - "dew_point": 284.34, - "uvi": 0.42, - "clouds": 100, - "visibility": 10000, - "wind_speed": 0.86, - "wind_deg": 359, - "wind_gust": 2.18, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.52, - "rain": { - "1h": 0.21 - } - }, - { - "dt": 1620741600, - "temp": 286.03, - "feels_like": 285.78, - "pressure": 1009, - "humidity": 92, - "dew_point": 284.22, - "uvi": 0.32, - "clouds": 100, - "visibility": 10000, - "wind_speed": 0.61, - "wind_deg": 7, - "wind_gust": 2.13, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.46 - }, - { - "dt": 1620745200, - "temp": 285.72, - "feels_like": 285.46, - "pressure": 1009, - "humidity": 93, - "dew_point": 284.09, - "uvi": 0.21, - "clouds": 100, - "visibility": 10000, - "wind_speed": 0.73, - "wind_deg": 41, - "wind_gust": 2, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "pop": 0.46, - "rain": { - "1h": 0.13 - } - }, - { - "dt": 1620748800, - "temp": 285.26, - "feels_like": 284.98, - "pressure": 1009, - "humidity": 94, - "dew_point": 283.76, - "uvi": 0.49, - "clouds": 100, - "visibility": 10000, - "wind_speed": 0.77, - "wind_deg": 36, - "wind_gust": 1.41, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.5 - }, - { - "dt": 1620752400, - "temp": 285.11, - "feels_like": 284.84, - "pressure": 1010, - "humidity": 95, - "dew_point": 283.69, - "uvi": 0.21, - "clouds": 100, - "visibility": 10000, - "wind_speed": 0.65, - "wind_deg": 328, - "wind_gust": 0.79, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.28 - }, - { - "dt": 1620756000, - "temp": 284.89, - "feels_like": 284.6, - "pressure": 1010, - "humidity": 95, - "dew_point": 283.47, - "uvi": 0.06, - "clouds": 100, - "visibility": 10000, - "wind_speed": 0.66, - "wind_deg": 358, - "wind_gust": 0.69, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.23 - }, - { - "dt": 1620759600, - "temp": 284.26, - "feels_like": 283.93, - "pressure": 1010, - "humidity": 96, - "dew_point": 283.01, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 0.1, - "wind_deg": 339, - "wind_gust": 0.53, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.11 - }, - { - "dt": 1620763200, - "temp": 284.05, - "feels_like": 283.7, - "pressure": 1011, - "humidity": 96, - "dew_point": 282.93, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 0.74, - "wind_deg": 235, - "wind_gust": 0.76, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04n" - } - ], - "pop": 0.11 - }, - { - "dt": 1620766800, - "temp": 284.08, - "feels_like": 283.74, - "pressure": 1011, - "humidity": 96, - "dew_point": 282.85, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 0.77, - "wind_deg": 247, - "wind_gust": 0.76, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04n" - } - ], - "pop": 0.09 - }, - { - "dt": 1620770400, - "temp": 283.67, - "feels_like": 283.31, - "pressure": 1010, - "humidity": 97, - "dew_point": 282.57, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.02, - "wind_deg": 258, - "wind_gust": 0.97, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04n" - } - ], - "pop": 0.09 - }, - { - "dt": 1620774000, - "temp": 282.99, - "feels_like": 282.99, - "pressure": 1011, - "humidity": 97, - "dew_point": 281.94, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.21, - "wind_deg": 256, - "wind_gust": 1.18, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04n" - } - ], - "pop": 0.06 - }, - { - "dt": 1620777600, - "temp": 282.74, - "feels_like": 282.74, - "pressure": 1011, - "humidity": 97, - "dew_point": 281.74, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 0.99, - "wind_deg": 248, - "wind_gust": 1.04, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04n" - } - ], - "pop": 0.06 - }, - { - "dt": 1620781200, - "temp": 282.3, - "feels_like": 282.3, - "pressure": 1011, - "humidity": 97, - "dew_point": 281.29, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.1, - "wind_deg": 248, - "wind_gust": 1.13, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04n" - } - ], - "pop": 0 - }, - { - "dt": 1620784800, - "temp": 282.39, - "feels_like": 282.39, - "pressure": 1011, - "humidity": 97, - "dew_point": 281.34, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.17, - "wind_deg": 254, - "wind_gust": 1.11, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04n" - } - ], - "pop": 0 - }, - { - "dt": 1620788400, - "temp": 282.81, - "feels_like": 282.54, - "pressure": 1011, - "humidity": 97, - "dew_point": 281.79, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.38, - "wind_deg": 257, - "wind_gust": 1.69, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04n" - } - ], - "pop": 0 - }, - { - "dt": 1620792000, - "temp": 282.98, - "feels_like": 282.59, - "pressure": 1011, - "humidity": 97, - "dew_point": 282.01, - "uvi": 0, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.51, - "wind_deg": 259, - "wind_gust": 2.36, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0 - }, - { - "dt": 1620795600, - "temp": 283.58, - "feels_like": 283.21, - "pressure": 1011, - "humidity": 97, - "dew_point": 282.52, - "uvi": 0.03, - "clouds": 100, - "visibility": 10000, - "wind_speed": 1.4, - "wind_deg": 257, - "wind_gust": 5.13, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0 - }, - { - "dt": 1620799200, - "temp": 284.32, - "feels_like": 283.97, - "pressure": 1011, - "humidity": 95, - "dew_point": 283.03, - "uvi": 0.09, - "clouds": 100, - "visibility": 10000, - "wind_speed": 2.32, - "wind_deg": 268, - "wind_gust": 6.01, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0 - }, - { - "dt": 1620802800, - "temp": 284.39, - "feels_like": 284.05, - "pressure": 1011, - "humidity": 95, - "dew_point": 283.04, - "uvi": 0.61, - "clouds": 100, - "visibility": 10000, - "wind_speed": 2.95, - "wind_deg": 277, - "wind_gust": 5.93, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.01 - }, - { - "dt": 1620806400, - "temp": 284.23, - "feels_like": 283.87, - "pressure": 1011, - "humidity": 95, - "dew_point": 282.9, - "uvi": 1.09, - "clouds": 100, - "visibility": 5163, - "wind_speed": 3.24, - "wind_deg": 281, - "wind_gust": 6.01, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.02 - }, - { - "dt": 1620810000, - "temp": 283.95, - "feels_like": 283.59, - "pressure": 1011, - "humidity": 96, - "dew_point": 282.73, - "uvi": 1.63, - "clouds": 100, - "visibility": 590, - "wind_speed": 3.33, - "wind_deg": 286, - "wind_gust": 6.76, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.16 - }, - { - "dt": 1620813600, - "temp": 284.19, - "feels_like": 283.8, - "pressure": 1011, - "humidity": 94, - "dew_point": 282.7, - "uvi": 1.61, - "clouds": 100, - "visibility": 3910, - "wind_speed": 3.53, - "wind_deg": 289, - "wind_gust": 6.77, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.34 - }, - { - "dt": 1620817200, - "temp": 284.67, - "feels_like": 284.23, - "pressure": 1011, - "humidity": 90, - "dew_point": 282.59, - "uvi": 1.82, - "clouds": 100, - "visibility": 10000, - "wind_speed": 4.02, - "wind_deg": 295, - "wind_gust": 6.45, - "weather": [ - { - "id": 804, - "main": "Clouds", - "description": "overcast clouds", - "icon": "04d" - } - ], - "pop": 0.34 - } - ], - "daily": [ - { - "dt": 1620644400, - "sunrise": 1620618337, - "sunset": 1620673188, - "moonrise": 1620617640, - "moonset": 1620668340, - "moon_phase": 0.96, - "temp": { - "day": 292.05, - "min": 284.57, - "max": 292.05, - "night": 284.57, - "eve": 286.27, - "morn": 288.11 - }, - "feels_like": { - "day": 291.51, - "night": 288.09, - "eve": 286.17, - "morn": 288.09 - }, - "pressure": 1009, - "humidity": 58, - "dew_point": 283.62, - "wind_speed": 5.42, - "wind_deg": 210, - "wind_gust": 11.48, - "weather": [ - { - "id": 501, - "main": "Rain", - "description": "moderate rain", - "icon": "10d" - } - ], - "clouds": 99, - "pop": 1, - "rain": 8.86, - "uvi": 5.03 - }, - { - "dt": 1620730800, - "sunrise": 1620704645, - "sunset": 1620759679, - "moonrise": 1620705000, - "moonset": 1620758820, - "moon_phase": 0, - "temp": { - "day": 286.49, - "min": 283.43, - "max": 287.93, - "night": 284.08, - "eve": 285.11, - "morn": 283.53 - }, - "feels_like": { - "day": 286.2, - "night": 283.16, - "eve": 284.84, - "morn": 283.16 - }, - "pressure": 1009, - "humidity": 89, - "dew_point": 284.16, - "wind_speed": 2.58, - "wind_deg": 311, - "wind_gust": 6.85, - "weather": [ - { - "id": 501, - "main": "Rain", - "description": "moderate rain", - "icon": "10d" - } - ], - "clouds": 100, - "pop": 1, - "rain": 5.08, - "uvi": 0.9 - }, - { - "dt": 1620817200, - "sunrise": 1620790954, - "sunset": 1620846169, - "moonrise": 1620792540, - "moonset": 1620849300, - "moon_phase": 0.02, - "temp": { - "day": 284.67, - "min": 282.3, - "max": 285.09, - "night": 283.05, - "eve": 284.4, - "morn": 283.58 - }, - "feels_like": { - "day": 284.23, - "night": 283.21, - "eve": 283.96, - "morn": 283.21 - }, - "pressure": 1011, - "humidity": 90, - "dew_point": 282.59, - "wind_speed": 4.02, - "wind_deg": 295, - "wind_gust": 6.77, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "clouds": 100, - "pop": 0.66, - "rain": 0.4, - "uvi": 1.82 - }, - { - "dt": 1620903600, - "sunrise": 1620877264, - "sunset": 1620932658, - "moonrise": 1620880380, - "moonset": 1620939720, - "moon_phase": 0.05, - "temp": { - "day": 283.28, - "min": 282.49, - "max": 285.55, - "night": 282.93, - "eve": 285.29, - "morn": 282.84 - }, - "feels_like": { - "day": 282.8, - "night": 282.84, - "eve": 284.75, - "morn": 282.84 - }, - "pressure": 1009, - "humidity": 94, - "dew_point": 281.78, - "wind_speed": 1.84, - "wind_deg": 76, - "wind_gust": 2.71, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "clouds": 100, - "pop": 0.88, - "rain": 2.24, - "uvi": 1.83 - }, - { - "dt": 1620990000, - "sunrise": 1620963577, - "sunset": 1621019147, - "moonrise": 1620968640, - "moonset": 0, - "moon_phase": 0.08, - "temp": { - "day": 288.88, - "min": 280.84, - "max": 289.48, - "night": 282.34, - "eve": 286.25, - "morn": 283.31 - }, - "feels_like": { - "day": 288.34, - "night": 282.71, - "eve": 285.89, - "morn": 282.71 - }, - "pressure": 1009, - "humidity": 70, - "dew_point": 282.95, - "wind_speed": 1.42, - "wind_deg": 29, - "wind_gust": 1.53, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "clouds": 99, - "pop": 0.99, - "rain": 4.46, - "uvi": 5.25 - }, - { - "dt": 1621076400, - "sunrise": 1621049892, - "sunset": 1621105635, - "moonrise": 1621057380, - "moonset": 1621029840, - "moon_phase": 0.11, - "temp": { - "day": 289.89, - "min": 279.6, - "max": 289.89, - "night": 280.88, - "eve": 283.71, - "morn": 283.11 - }, - "feels_like": { - "day": 289.45, - "night": 283.11, - "eve": 283.3, - "morn": 283.11 - }, - "pressure": 1011, - "humidity": 70, - "dew_point": 283.91, - "wind_speed": 4.57, - "wind_deg": 268, - "wind_gust": 6.74, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "clouds": 82, - "pop": 1, - "rain": 4.39, - "uvi": 6 - }, - { - "dt": 1621162800, - "sunrise": 1621136208, - "sunset": 1621192121, - "moonrise": 1621146780, - "moonset": 1621119600, - "moon_phase": 0.14, - "temp": { - "day": 283.85, - "min": 279.28, - "max": 286.59, - "night": 281.76, - "eve": 283.77, - "morn": 282.96 - }, - "feels_like": { - "day": 283.38, - "night": 282.96, - "eve": 283.32, - "morn": 282.96 - }, - "pressure": 1010, - "humidity": 92, - "dew_point": 281.96, - "wind_speed": 5.17, - "wind_deg": 238, - "wind_gust": 10.9, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "clouds": 95, - "pop": 0.98, - "rain": 4.05, - "uvi": 6 - }, - { - "dt": 1621249200, - "sunrise": 1621222527, - "sunset": 1621278607, - "moonrise": 1621236780, - "moonset": 1621208820, - "moon_phase": 0.17, - "temp": { - "day": 285.26, - "min": 279.41, - "max": 285.41, - "night": 279.41, - "eve": 283.03, - "morn": 281.49 - }, - "feels_like": { - "day": 284.67, - "night": 278.95, - "eve": 281.18, - "morn": 278.95 - }, - "pressure": 1015, - "humidity": 82, - "dew_point": 281.75, - "wind_speed": 5.53, - "wind_deg": 267, - "wind_gust": 10.12, - "weather": [ - { - "id": 500, - "main": "Rain", - "description": "light rain", - "icon": "10d" - } - ], - "clouds": 91, - "pop": 1, - "rain": 6.25, - "uvi": 6 - } - ], - "alerts": [ - { - "sender_name": "Deutscher Wetterdienst", - "event": "WINDBÖEN", - "start": 1620643620, - "end": 1620673200, - "description": "Es treten oberhalb 400 m Windböen mit Geschwindigkeiten bis 60 km/h (17m/s, 33kn, Bft 7) aus südwestlicher Richtung auf. In exponierten Lagen muss mit Sturmböen bis 70 km/h (20m/s, 38kn, Bft 8) gerechnet werden." - }, - { - "sender_name": "Deutscher Wetterdienst", - "event": "wind gusts", - "start": 1620612000, - "end": 1620662400, - "description": "There is a risk of wind gusts (level 1 of 4).\nMax. gusts: \u003c 60 km/h; Wind direction: south-west; Increased gusts: in exposed locations \u003c 70 km/h" - } - ] -} From cc698838bffb2e4002194ae6c8faf3c8b37d2228 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Sat, 18 Sep 2021 18:45:21 +0200 Subject: [PATCH 11/13] remove real auth tests Signed-off-by: Bernd Weymann --- .../internal/handler/AuthProbes.java | 299 ------------------ .../internal/handler/AuthTest.java | 245 -------------- 2 files changed, 544 deletions(-) delete mode 100644 bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AuthProbes.java delete mode 100644 bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AuthTest.java diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AuthProbes.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AuthProbes.java deleted file mode 100644 index c2f3b6895325d..0000000000000 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AuthProbes.java +++ /dev/null @@ -1,299 +0,0 @@ -/** - * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.handler; - -import static org.openhab.binding.bmwconnecteddrive.internal.utils.HTTPConstants.*; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jetty.client.HttpClient; -import org.eclipse.jetty.client.api.ContentResponse; -import org.eclipse.jetty.client.api.Request; -import org.eclipse.jetty.client.util.StringContentProvider; -import org.eclipse.jetty.http.HttpField; -import org.eclipse.jetty.http.HttpFields; -import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.util.MultiMap; -import org.eclipse.jetty.util.UrlEncoded; -import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConfiguration; -import org.openhab.binding.bmwconnecteddrive.internal.dto.auth.AuthResponse; -import org.openhab.binding.bmwconnecteddrive.internal.utils.BimmerConstants; -import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants; -import org.openhab.binding.bmwconnecteddrive.internal.utils.Converter; -import org.openhab.core.io.net.http.HttpClientFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * The {@link AuthProbes} This class holds the important constants for the BMW Connected Drive Authorization. - * They - * are taken from the Bimmercode from github {@link https://github.com/bimmerconnected/bimmer_connected} - * File defining these constants - * {@link https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/account.py} - * https://customer.bmwgroup.com/one/app/oauth.js - * - * @author Bernd Weymann - Initial contribution - * https://github.com/weymann/openhab-addons/blob/80001e89d8b03ea633b2279dcff81f322a5ad6aa/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveProxy.java - * - */ -@NonNullByDefault -public class AuthProbes { - private final Logger logger = LoggerFactory.getLogger(AuthProbes.class); - private final Token token = new Token(); - private HttpClient httpClient; - private HttpClient authHttpClient; - private String authUri; - private String legacyAuthUri; - private ConnectedDriveConfiguration configuration; - private String clientId = "dbf0a542-ebd1-4ff0-a9a7-55172fbfce35"; - - /** - * URLs taken from https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/const.py - * - * """URLs for different services and error code mapping.""" - * - * AUTH_URL = 'https://customer.bmwgroup.com/{gcdm_oauth_endpoint}/authenticate' - * AUTH_URL_LEGACY = 'https://{server}/gcdm/oauth/token' - * BASE_URL = 'https://{server}/webapi/v1' - * - * VEHICLES_URL = BASE_URL + '/user/vehicles' - * VEHICLE_VIN_URL = VEHICLES_URL + '/{vin}' - * VEHICLE_STATUS_URL = VEHICLE_VIN_URL + '/status' - * REMOTE_SERVICE_STATUS_URL = VEHICLE_VIN_URL + '/serviceExecutionStatus?serviceType={service_type}' - * REMOTE_SERVICE_URL = VEHICLE_VIN_URL + "/executeService" - * VEHICLE_IMAGE_URL = VEHICLE_VIN_URL + "/image?width={width}&height={height}&view={view}" - * VEHICLE_POI_URL = VEHICLE_VIN_URL + '/sendpoi' - * - * } - */ - String baseUrl; - String legacyUrl; - String vehicleStatusAPI = "/status"; - String lastTripAPI = "/statistics/lastTrip"; - String allTripsAPI = "/statistics/allTrips"; - String chargeAPI = "/chargingprofile"; - String destinationAPI = "/destinations"; - String imageAPI = "/image"; - String rangeMapAPI = "/rangemap"; - String serviceExecutionAPI = "/executeService"; - String serviceExecutionStateAPI = "/serviceExecutionStatus"; - - public AuthProbes(HttpClientFactory httpClientFactory, ConnectedDriveConfiguration config) { - httpClient = httpClientFactory.getCommonHttpClient(); - authHttpClient = httpClientFactory.createHttpClient(AUTH_HTTP_CLIENT_NAME); - authHttpClient.setFollowRedirects(false); - try { - authHttpClient.start(); - } catch (Exception e) { - logger.warn("Auth Http Client cannot be started"); - } - configuration = config; - // generate URI for Authorization - // see https://customer.bmwgroup.com/one/app/oauth.js - StringBuilder uri = new StringBuilder(); - uri.append("https://customer.bmwgroup.com"); - if (BimmerConstants.LEGACY_AUTH_SERVER_MAP.equals(configuration.region)) { - uri.append("/gcdm/usa/oauth/authenticate"); - } else { - uri.append("/gcdm/oauth/authenticate"); - } - authUri = uri.toString(); - - StringBuilder legacyAuth = new StringBuilder(); - legacyAuth.append("https://"); - legacyAuth.append(BimmerConstants.AUTH_SERVER_MAP.get(configuration.region)); - legacyAuth.append(BimmerConstants.OAUTH_ENDPOINT); - legacyAuthUri = legacyAuth.toString(); - baseUrl = "https://" + getRegionServer() + "/webapi/v1/user/vehicles/"; - legacyUrl = "https://" + getRegionServer() + "/api/vehicle/dynamic/v1/"; - } - - private String getRegionServer() { - String retVal = BimmerConstants.LEGACY_AUTH_SERVER_MAP.get(configuration.region); - if (retVal != null) { - return retVal; - } else { - return Constants.INVALID; - } - } - - /** - * Gets new token if old one is expired or invalid. In case of error the token remains. - * So if token refresh fails the corresponding requests will also fail and update the - * Thing status accordingly. - * - * @return token - */ - public Token getToken() { - if (token.isExpired() || !token.isValid()) { - legacyUpdateToken(); - } - return token; - } - - /** - * Authorize at BMW Connected Drive Portal and get Token - * - * @return - */ - private synchronized void jettyUpdateToken() { - logger.info("updateToken - start"); - Request req = authHttpClient.POST(authUri); - - req.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED); - req.header(HttpHeader.CONNECTION, KEEP_ALIVE); - req.header(HttpHeader.HOST, BimmerConstants.LEGACY_AUTH_SERVER_MAP.get(configuration.region)); - req.header(HttpHeader.AUTHORIZATION, BimmerConstants.AUTHORIZATION_VALUE_MAP.get(configuration.region)); - req.header(CREDENTIALS, BimmerConstants.LEGACY_CREDENTIAL_VALUES); - - MultiMap dataMap = new MultiMap(); - dataMap.add(CLIENT_ID, clientId); - dataMap.add(RESPONSE_TYPE, TOKEN); - dataMap.add(REDIRECT_URI, BimmerConstants.REDIRECT_URI_VALUE); - dataMap.add(SCOPE, BimmerConstants.SCOPE_VALUES); - dataMap.add(USERNAME, configuration.userName); - dataMap.add(PASSWORD, configuration.password); - String urlEncodedData = UrlEncoded.encode(dataMap, Charset.defaultCharset(), false); - req.header(CONTENT_LENGTH, Integer.toString(urlEncodedData.length())); - req.content(new StringContentProvider(urlEncodedData)); - try { - ContentResponse contentResponse = req.timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS).send(); - // Status needs to be 302 - Response is stored in Header - if (contentResponse.getStatus() == 302) { - HttpFields fields = contentResponse.getHeaders(); - HttpField field = fields.getField(HttpHeader.LOCATION); - tokenFromUrl(field.getValue()); - } else { - logger.debug("Authorization status {} reason {}", contentResponse.getStatus(), - contentResponse.getReason()); - } - } catch (InterruptedException | ExecutionException | TimeoutException e) { - logger.debug("Authorization exception: {}", e.getMessage()); - StackTraceElement[] trace = e.getStackTrace(); - for (int i = 0; i < trace.length; i++) { - logger.info("{}", trace[i]); - } - } - logger.info("updateToken - finish"); - } - - public synchronized void updateToken() { - try { - URL url = new URL("https://customer.bmwgroup.com/gcdm/oauth/authenticate"); - HttpURLConnection.setFollowRedirects(false); - HttpURLConnection con = (HttpURLConnection) url.openConnection(); - con.setRequestMethod("POST"); - - con.setRequestProperty(HttpHeader.CONTENT_TYPE.toString(), CONTENT_TYPE_URL_ENCODED); - con.setRequestProperty(HttpHeader.CONNECTION.toString(), KEEP_ALIVE); - con.setRequestProperty(HttpHeader.HOST.toString(), - BimmerConstants.LEGACY_AUTH_SERVER_MAP.get(BimmerConstants.AUTH_SERVER_ROW)); - con.setRequestProperty(HttpHeader.AUTHORIZATION.toString(), - BimmerConstants.AUTHORIZATION_VALUE_MAP.get(configuration.region)); - con.setRequestProperty(CREDENTIALS, BimmerConstants.LEGACY_CREDENTIAL_VALUES); - con.setDoOutput(true); - - MultiMap dataMap = new MultiMap(); - dataMap.add(CLIENT_ID, clientId); - dataMap.add(RESPONSE_TYPE, TOKEN); - dataMap.add(REDIRECT_URI, BimmerConstants.REDIRECT_URI_VALUE); - dataMap.add(SCOPE, BimmerConstants.SCOPE_VALUES); - dataMap.add(USERNAME, configuration.userName); - dataMap.add(PASSWORD, configuration.password); - String urlEncodedData = UrlEncoded.encode(dataMap, Charset.defaultCharset(), false); - OutputStream os = con.getOutputStream(); - byte[] input = urlEncodedData.getBytes("utf-8"); - os.write(input, 0, input.length); - BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream(), "utf-8")); - StringBuilder response = new StringBuilder(); - String responseLine = null; - while ((responseLine = br.readLine()) != null) { - response.append(responseLine.trim()); - } - logger.info("Response Code {} Message {} ", con.getResponseCode(), con.getResponseMessage()); - tokenFromUrl(con.getHeaderField(HttpHeader.LOCATION.toString())); - } catch (IOException e) { - logger.warn("{}", e.getMessage()); - } - } - - private synchronized void legacyUpdateToken() { - try { - logger.info("Auth {}", legacyAuthUri); - URL url = new URL(legacyAuthUri); - HttpURLConnection.setFollowRedirects(false); - HttpURLConnection con = (HttpURLConnection) url.openConnection(); - con.setRequestMethod("POST"); - - con.setRequestProperty(HttpHeader.CONTENT_TYPE.toString(), CONTENT_TYPE_URL_ENCODED); - con.setRequestProperty(HttpHeader.CONNECTION.toString(), KEEP_ALIVE); - con.setRequestProperty(HttpHeader.HOST.toString(), - BimmerConstants.LEGACY_AUTH_SERVER_MAP.get(BimmerConstants.LEGACY_AUTH_SERVER_ROW)); - // con.setRequestProperty(HttpHeader.AUTHORIZATION.toString(), BimmerConstants.LEGACY_AUTHORIZATION_VALUE); - con.setRequestProperty(CREDENTIALS, BimmerConstants.LEGACY_CREDENTIAL_VALUES); - con.setDoOutput(true); - - MultiMap dataMap = new MultiMap(); - dataMap.add("grant_type", "password"); - dataMap.add(SCOPE, BimmerConstants.SCOPE_VALUES); - dataMap.add(USERNAME, configuration.userName); - dataMap.add(PASSWORD, configuration.password); - String urlEncodedData = UrlEncoded.encode(dataMap, Charset.defaultCharset(), false); - OutputStream os = con.getOutputStream(); - byte[] input = urlEncodedData.getBytes("utf-8"); - os.write(input, 0, input.length); - BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream(), "utf-8")); - StringBuilder response = new StringBuilder(); - String responseLine = null; - while ((responseLine = br.readLine()) != null) { - response.append(responseLine.trim()); - } - logger.info("Response Code {} Message {} ", con.getResponseCode(), con.getResponseMessage()); - // logger.info("Response {}", response.toString()); - AuthResponse authResponse = Converter.getGson().fromJson(response.toString(), AuthResponse.class); - // token.setToken(authResponse.access_token); - // token.setType(authResponse.token_type); - // token.setExpiration(authResponse.expires_in); - } catch (IOException e) { - logger.warn("{}", e.getMessage()); - } - } - - void tokenFromUrl(String encodedUrl) { - MultiMap tokenMap = new MultiMap(); - UrlEncoded.decodeTo(encodedUrl, tokenMap, StandardCharsets.US_ASCII); - tokenMap.forEach((key, value) -> { - if (value.size() > 0) { - String val = value.get(0); - if (key.endsWith(ACCESS_TOKEN)) { - token.setToken(val.toString()); - } else if (key.equals(EXPIRES_IN)) { - token.setExpiration(Integer.parseInt(val.toString())); - } else if (key.equals(TOKEN_TYPE)) { - token.setType(val.toString()); - } - } - }); - } -} diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AuthTest.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AuthTest.java deleted file mode 100644 index 07efb6c6c8104..0000000000000 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/test/java/org/openhab/binding/bmwconnecteddrive/internal/handler/AuthTest.java +++ /dev/null @@ -1,245 +0,0 @@ -/** - * Copyright (c) 2010-2021 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.bmwconnecteddrive.internal.handler; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.*; -import static org.openhab.binding.bmwconnecteddrive.internal.utils.HTTPConstants.*; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.ProtocolException; -import java.net.URL; -import java.net.URLEncoder; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.Optional; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jetty.client.HttpClient; -import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.util.MultiMap; -import org.eclipse.jetty.util.UrlEncoded; -import org.eclipse.jetty.util.ssl.SslContextFactory; -import org.junit.jupiter.api.Test; -import org.openhab.binding.bmwconnecteddrive.internal.ConnectedDriveConfiguration; -import org.openhab.binding.bmwconnecteddrive.internal.VehicleConfiguration; -import org.openhab.binding.bmwconnecteddrive.internal.utils.BimmerConstants; -import org.openhab.binding.bmwconnecteddrive.internal.utils.Constants; -import org.openhab.binding.bmwconnecteddrive.internal.utils.HTTPConstants; -import org.openhab.core.io.net.http.HttpClientFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * The {@link AuthTest} is responsible for handling commands, which are - * sent to one of the channels. - * - * @author Bernd Weymann - Initial contribution - */ -@NonNullByDefault -public class AuthTest { - private final Logger logger = LoggerFactory.getLogger(AuthTest.class); - - @Test - public void testTokenDecoding() { - String headerValue = "https://www.bmw-connecteddrive.com/app/static/external-dispatch.html#access_token=SfXKgkEXeeFJkVqdD4XMmfUU224MRuyh&token_type=Bearer&expires_in=7199"; - HttpClientFactory hcf = mock(HttpClientFactory.class); - when(hcf.getCommonHttpClient()).thenReturn(mock(HttpClient.class)); - when(hcf.createHttpClient(HTTPConstants.AUTH_HTTP_CLIENT_NAME)).thenReturn(mock(HttpClient.class)); - ConnectedDriveConfiguration config = new ConnectedDriveConfiguration(); - config.region = BimmerConstants.REGION_ROW; - ConnectedDriveProxy dcp = new ConnectedDriveProxy(hcf, config); - dcp.tokenFromUrl(headerValue); - Token t = dcp.getToken(); - assertEquals("Bearer SfXKgkEXeeFJkVqdD4XMmfUU224MRuyh", t.getBearerToken(), "Token"); - } - - @Test - public void testRealTokenUpdate() { - ConnectedDriveConfiguration config = new ConnectedDriveConfiguration(); - config.region = BimmerConstants.REGION_ROW; - config.userName = "bla"; - config.password = "blub"; - HttpClientFactory hcf = mock(HttpClientFactory.class); - when(hcf.getCommonHttpClient()).thenReturn(mock(HttpClient.class)); - when(hcf.createHttpClient(HTTPConstants.AUTH_HTTP_CLIENT_NAME)).thenReturn(mock(HttpClient.class)); - ConnectedDriveProxy dcp = new ConnectedDriveProxy(hcf, config); - Token t = dcp.getToken(); - logger.info("Token {}", t.getBearerToken()); - logger.info("Expires {}", t.isExpired()); - } - - public void testJavaHttpAuth() { - ConnectedDriveConfiguration config = new ConnectedDriveConfiguration(); - config.region = BimmerConstants.REGION_ROW; - config.userName = "bla"; - config.password = "bla"; - - final StringBuilder legacyAuth = new StringBuilder(); - legacyAuth.append("https://"); - legacyAuth.append(BimmerConstants.AUTH_SERVER_MAP.get(config.region)); - legacyAuth.append(BimmerConstants.OAUTH_ENDPOINT); - URL url; - try { - - final MultiMap dataMap = new MultiMap(); - dataMap.add("grant_type", "password"); - dataMap.add(SCOPE, BimmerConstants.LEGACY_SCOPE_VALUES); - dataMap.add(USERNAME, config.userName); - dataMap.add(PASSWORD, config.password); - - String urlContent = UrlEncoded.encode(dataMap, StandardCharsets.UTF_8, false); - url = new URL(legacyAuth.toString() + "?" + urlContent); - HttpURLConnection con = (HttpURLConnection) url.openConnection(); - con.setRequestMethod("POST"); - con.setRequestProperty(HttpHeader.CONTENT_LENGTH.asString(), Integer.toString(124)); - con.setRequestProperty(HttpHeader.CONTENT_TYPE.asString(), "application/x-www-form-urlencoded"); - // System.out.println(con.getHeaderField(HttpHeader.CONTENT_LENGTH.asString())); - // con.setRequestProperty(HttpHeader.CONNECTION.asString(), KEEP_ALIVE); - con.setRequestProperty(HttpHeader.HOST.asString(), BimmerConstants.API_SERVER_MAP.get(config.region)); - con.setRequestProperty(HttpHeader.AUTHORIZATION.asString(), - BimmerConstants.LEGACY_AUTHORIZATION_VALUE_MAP.get(config.region)); - con.setRequestProperty(CREDENTIALS, BimmerConstants.LEGACY_CREDENTIAL_VALUES); - con.setRequestProperty(HttpHeader.REFERER.asString(), BimmerConstants.LEGACY_REFERER_URL); - int status = con.getResponseCode(); - if (status < 400) { - BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); - String inputLine; - StringBuffer content = new StringBuffer(); - while ((inputLine = in.readLine()) != null) { - content.append(inputLine); - } - in.close(); - } - con.disconnect(); - } catch (MalformedURLException e) { - } catch (ProtocolException e) { - } catch (IOException e) { - } - } - - @Test - public void urlencodeTest() { - String expectedResult = "client_id=31c357a0-7a1d-4590-aa99-33b97244d048&response_type=code&redirect_uri=com.bmw.connected%3A%2F%2Foauth&state=cEG9eLAIi6Nv-aaCAniziE_B6FPoobva3qr5gukilYw&nonce=login_nonce&scope=openid+profile+email+offline_access+smacc+vehicle_data+perseus+dlm+svds+cesim+vsapi+remote_services+fupo+authenticate_user&authorization=G0s3x-LE682iWMJ3WSLm0TmB2R4.%2AAAJTSQACMDIAAlNLABw2c0llQVVyaTB5OFdpUlptQjVtWXhmaWNVTzQ9AAR0eXBlAANDVFMAAlMxAAIwMQ..%2A"; - - MultiMap baseValues = new MultiMap(); - baseValues.add(CLIENT_ID, Constants.EMPTY + BimmerConstants.CLIENT_ID.get("ROW")); - baseValues.add(RESPONSE_TYPE, CODE); - baseValues.add(REDIRECT_URI, BimmerConstants.REDIRECT_URI_VALUE); - baseValues.add("state", Constants.EMPTY + BimmerConstants.STATE.get("ROW")); - baseValues.add("nonce", "login_nonce"); - baseValues.add(SCOPE, BimmerConstants.SCOPE_VALUES); - - MultiMap codeChallenge = new MultiMap(); - codeChallenge.addAllValues(baseValues); - // codeChallenge.put("authorization", authCode); - logger.info("Code MultiMap {}", codeChallenge); - - // String codeEncoded = UrlEncoded.encode(codeChallenge, Charset.defaultCharset(), false); - String enc = URLEncoder.encode(codeChallenge.toString(), Charset.defaultCharset()); - logger.info("Code Url enc {}", enc); - - System.out.println(Charset.availableCharsets()); - String authCodeGot = "G0s3x-LE682iWMJ3WSLm0TmB2R4.*AAJTSQACMDIAAlNLABw2c0llQVVyaTB5OFdpUlptQjVtWXhmaWNVTzQ9AAR0eXBlAANDVFMAAlMxAAIwMQ..*"; - logger.info("Encoded {}", UrlEncoded.encodeString(authCodeGot)); - - codeChallenge.put("authorization", authCodeGot); - logger.info("Encoded {}", UrlEncoded.encode(codeChallenge, Charset.forName("UTF-8"), false)); - String authCodeSent = "G0s3x-LE682iWMJ3WSLm0TmB2R4.%2AAAJTSQACMDIAAlNLABw2c0llQVVyaTB5OFdpUlptQjVtWXhmaWNVTzQ9AAR0eXBlAANDVFMAAlMxAAIwMQ..%2A"; - } - - public void testLegacyJetty() { - SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); - HttpClient client = new HttpClient(sslContextFactory); - client.setIdleTimeout(20000); - try { - client.start(); - } catch (Exception e) { - logger.info("Client Start Exception {}", e.getMessage()); - } - ConnectedDriveConfiguration c = new ConnectedDriveConfiguration(); - c.region = "ROW"; - c.userName = "bla"; - c.password = "bla"; - - HttpClient hc = mock(HttpClient.class); - HttpClientFactory hct = mock(HttpClientFactory.class); - when(hct.getCommonHttpClient()).thenReturn(hc); - when(hct.createHttpClient(AUTH_HTTP_CLIENT_NAME)).thenReturn(hc); - ConnectedDriveProxy auth = new ConnectedDriveProxy(hct, c); - auth.updateToken(client); - } - - @Test - public void testRemote() { - // String profile = - // "{\"weeklyPlanner\":{\"climatizationEnabled\":false,\"chargingMode\":\"IMMEDIATE_CHARGING\",\"chargingPreferences\":\"CHARGING_WINDOW\",\"timer1\":{\"departureTime\":\"16:00\",\"timerEnabled\":false,\"weekdays\":[\"MONDAY\",\"TUESDAY\",\"WEDNESDAY\",\"THURSDAY\",\"FRIDAY\",\"SATURDAY\",\"SUNDAY\"]},\"timer2\":{\"departureTime\":\"12:02\",\"timerEnabled\":false,\"weekdays\":[\"SATURDAY\"]},\"timer3\":{\"departureTime\":\"13:03\",\"timerEnabled\":false,\"weekdays\":[\"SATURDAY\"]},\"overrideTimer\":{\"departureTime\":\"00:00\",\"timerEnabled\":false,\"weekdays\":[]},\"preferredChargingWindow\":{\"startTime\":\"10:00\",\"endTime\":\"15:00\"}}}"; - VehicleConfiguration vc = new VehicleConfiguration(); - vc.vin = "WBY1Z81040V905639"; - SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); - String profile = "{\"weeklyPlanner\":{\"climatizationEnabled\":false,\"chargingMode\":\"IMMEDIATE_CHARGING\",\"chargingPreferences\":\"CHARGING_WINDOW\",\"timer1\":{\"departureTime\":\"16:00\",\"timerEnabled\":false,\"weekdays\":[\"MONDAY\",\"TUESDAY\",\"WEDNESDAY\",\"THURSDAY\",\"FRIDAY\",\"SATURDAY\",\"SUNDAY\"]},\"timer2\":{\"departureTime\":\"12:02\",\"timerEnabled\":false,\"weekdays\":[\"SATURDAY\"]},\"timer3\":{\"departureTime\":\"13:03\",\"timerEnabled\":false,\"weekdays\":[\"SATURDAY\"]},\"overrideTimer\":{\"departureTime\":\"00:00\",\"timerEnabled\":false,\"weekdays\":[]},\"preferredChargingWindow\":{\"startTime\":\"10:00\",\"endTime\":\"15:00\"}}}"; - // String profile = - // "{\"type\":\"CHARGING_PROFILE\",\"weeklyPlanner\":{\"climatizationEnabled\":false,\"chargingMode\":\"IMMEDIATE_CHARGING\",\"chargingPreferences\":\"CHARGING_WINDOW\",\"timer1\":{\"departureTime\":\"16:00\",\"timerEnabled\":false,\"weekdays\":[\"MONDAY\",\"TUESDAY\",\"WEDNESDAY\",\"THURSDAY\",\"FRIDAY\",\"SATURDAY\",\"SUNDAY\"]},\"timer2\":{\"departureTime\":\"12:02\",\"timerEnabled\":false,\"weekdays\":[\"SATURDAY\"]},\"timer3\":{\"departureTime\":\"13:03\",\"timerEnabled\":false,\"weekdays\":[\"SATURDAY\"]},\"overrideTimer\":{\"departureTime\":\"00:00\",\"timerEnabled\":false,\"weekdays\":[]},\"preferredChargingWindow\":{\"startTime\":\"10:00\",\"endTime\":\"15:00\"}}}"; - String dataProfile = "{\"type\":\"CHARGING_PROFILE\",\"data\":" + profile + "}"; - HttpClient client = new HttpClient(sslContextFactory); - client.setIdleTimeout(20000); - try { - client.start(); - } catch (Exception e) { - logger.info("Client Start Exception {}", e.getMessage()); - } - ConnectedDriveConfiguration c = new ConnectedDriveConfiguration(); - c.region = "ROW"; - c.userName = "bla"; - c.password = "bla"; - - HttpClientFactory hct = mock(HttpClientFactory.class); - when(hct.getCommonHttpClient()).thenReturn(client); - when(hct.createHttpClient(AUTH_HTTP_CLIENT_NAME)).thenReturn(client); - VehicleHandler vh = mock(VehicleHandler.class); - when(vh.getConfiguration()).thenReturn(Optional.of(vc)); - ConnectedDriveProxy auth = new ConnectedDriveProxy(hct, c); - RemoteServiceHandler rsh = new RemoteServiceHandler(vh, auth); - rsh.setMyBmwApiUsage(true); - auth.updateToken(client); - // String url = "https://cocoapi.bmwgroup.com/eadrax-dcs/v1/send-to-car/send-to-car"; - String url = "https://cocoapi.bmwgroup.com" + ConnectedDriveProxy.REMOTE_SERVICE_EADRAX_BASE_URL + vc.vin - + "/vehicle-finder"; - // "/" - // + "charging-settings"; - // String url = "https://cocoapi.bmwgroup.com/eadrax-vrccs/v2/presentation/remote-history/" + vc.vin; - - String type = "{\"type\":\"LIGHTS\"}"; - // {"type":"CHARGING_PROFILE" - - auth.call(url, true, CONTENT_TYPE_JSON_ENCODED, dataProfile, rsh); - try { - Thread.sleep(2000); - } catch (InterruptedException e) { - } - // MultiMap dataMap = new MultiMap<>(); - // dataMap.put("data", profile); - // auth.call(url, true, CONTENT_TYPE_URL_ENCODED, UrlEncoded.encode(dataMap, StandardCharsets.UTF_8, false), - // rsh); - // try { - // Thread.sleep(2000); - // } catch (InterruptedException e) { - // // TODO Auto-generated catch block - // e.printStackTrace(); - // } - } -} From db77afef4793d498b6b0920d999a888ea0c2bbad Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Sat, 18 Sep 2021 19:15:26 +0200 Subject: [PATCH 12/13] remove repeated navigation callback message Signed-off-by: Bernd Weymann --- .../bmwconnecteddrive/internal/handler/VehicleHandler.java | 1 - 1 file changed, 1 deletion(-) diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java index 125e204098ef0..e6374d93cb38b 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java @@ -691,7 +691,6 @@ public void onError(NetworkError error) { public class NavigationStatusCallback implements StringResponseCallback { @Override public void onResponse(@Nullable String content) { - logger.debug("Navigation: {}", content); if (content != null) { try { NavigationContainer nav = Converter.getGson().fromJson(content, NavigationContainer.class); From 97d51a8279cea0b712ada3db8fff0116e07c2f0d Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Sat, 2 Oct 2021 21:45:43 +0200 Subject: [PATCH 13/13] applied review comments Signed-off-by: Bernd Weymann --- .../internal/handler/ConnectedDriveProxy.java | 179 ++++++++++-------- .../handler/RemoteServiceHandler.java | 31 +-- .../internal/handler/Token.java | 10 +- .../internal/handler/VehicleHandler.java | 3 +- .../internal/utils/BimmerConstants.java | 3 +- 5 files changed, 117 insertions(+), 109 deletions(-) diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveProxy.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveProxy.java index 90601c42758c0..dd383a3a470dc 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveProxy.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/ConnectedDriveProxy.java @@ -71,7 +71,6 @@ public class ConnectedDriveProxy { private final HttpClient httpClient; private final HttpClient authHttpClient; private final ConnectedDriveConfiguration configuration; - private String clientId = "dbf0a542-ebd1-4ff0-a9a7-55172fbfce35"; /** * URLs taken from https://github.com/bimmerconnected/bimmer_connected/blob/master/bimmer_connected/const.py @@ -91,20 +90,13 @@ public class ConnectedDriveProxy { final String rangeMapAPI = "/rangemap"; final String serviceExecutionAPI = "/executeService"; final String serviceExecutionStateAPI = "/serviceExecutionStatus"; - public final static String REMOTE_SERVICE_EADRAX_BASE_URL = "/eadrax-vrccs/v2/presentation/remote-commands/"; // '/{vin}/{service_type}' - final String REMOTE_SERVICE_EADRAX_STATUS_URL = REMOTE_SERVICE_EADRAX_BASE_URL + "eventStatus?eventId={event_id}"; - final String VEHICLE_EADRAX_POI_URL = "/eadrax-dcs/v1/send-to-car/send-to-car"; + public static final String REMOTE_SERVICE_EADRAX_BASE_URL = "/eadrax-vrccs/v2/presentation/remote-commands/"; // '/{vin}/{service_type}' + final String remoteServiceEADRXstatusUrl = REMOTE_SERVICE_EADRAX_BASE_URL + "eventStatus?eventId={event_id}"; + final String vehicleEADRXPoiUrl = "/eadrax-dcs/v1/send-to-car/send-to-car"; public ConnectedDriveProxy(HttpClientFactory httpClientFactory, ConnectedDriveConfiguration config) { httpClient = httpClientFactory.getCommonHttpClient(); authHttpClient = httpClientFactory.createHttpClient(AUTH_HTTP_CLIENT_NAME); - if (!authHttpClient.isStarted()) { - try { - authHttpClient.start(); - } catch (Exception e) { - logger.debug("Auth client start failed"); - } - } configuration = config; vehicleUrl = "https://" + BimmerConstants.API_SERVER_MAP.get(configuration.region) + "/webapi/v1/user/vehicles"; @@ -255,16 +247,16 @@ RemoteServiceHandler getRemoteServiceHandler(VehicleHandler vehicleHandler) { * @return token */ public Token getToken() { - if (token.isExpired() || !token.isValid()) { + if (!token.isValid()) { if (configuration.preferMyBmw) { - if (!updateToken(authHttpClient)) { + if (!updateToken()) { if (!updateLegacyToken()) { logger.debug("Authorization failed!"); } } } else { if (!updateLegacyToken()) { - if (!updateToken(authHttpClient)) { + if (!updateToken()) { logger.debug("Authorization failed!"); } } @@ -276,26 +268,80 @@ public Token getToken() { return token; } - public synchronized boolean updateToken(HttpClient client) { - if (!client.isStarted()) { - try { - client.start(); - } catch (Exception e) { - logger.debug("Authorization client cannot be started"); - return false; - } - } + public synchronized boolean updateToken() { if (BimmerConstants.REGION_CHINA.equals(configuration.region)) { - // region China stays on fallback solution + // region China currently not supported for MyBMW API logger.debug("Region {} not supported yet for MyBMW Login", BimmerConstants.REGION_CHINA); return false; } + if (!startAuthClient()) { + return false; + } // else continue String authUri = "https://" + BimmerConstants.AUTH_SERVER_MAP.get(configuration.region) + BimmerConstants.OAUTH_ENDPOINT; - Request authRequest = client.POST(authUri); + Request authRequest = authHttpClient.POST(authUri); authRequest.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED); + MultiMap authChallenge = getTokenBaseValues(); + authChallenge.addAllValues(getTokenAuthValues()); + String authEncoded = UrlEncoded.encode(authChallenge, Charset.defaultCharset(), false); + authRequest.content(new StringContentProvider(authEncoded)); + try { + ContentResponse authResponse = authRequest.timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS).send(); + String authResponseString = URLDecoder.decode(authResponse.getContentAsString(), Charset.defaultCharset()); + String authCode = getAuthCode(authResponseString); + if (authCode != Constants.EMPTY) { + MultiMap codeChallenge = getTokenBaseValues(); + codeChallenge.put(AUTHORIZATION, authCode); + + Request codeRequest = authHttpClient.POST(authUri).followRedirects(false); + codeRequest.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED); + String codeEncoded = UrlEncoded.encode(codeChallenge, Charset.defaultCharset(), false); + codeRequest.content(new StringContentProvider(codeEncoded)); + ContentResponse codeResponse = codeRequest.timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS).send(); + String code = ConnectedDriveProxy.codeFromUrl(codeResponse.getHeaders().get(HttpHeader.LOCATION)); + + // Get Token + String tokenUrl = "https://" + BimmerConstants.AUTH_SERVER_MAP.get(configuration.region) + + BimmerConstants.TOKEN_ENDPOINT; + + Request tokenRequest = authHttpClient.POST(tokenUrl).followRedirects(false); + tokenRequest.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED); + tokenRequest.header(HttpHeader.AUTHORIZATION, + BimmerConstants.AUTHORIZATION_VALUE_MAP.get(configuration.region)); + String tokenEncoded = UrlEncoded.encode(getTokenValues(code), Charset.defaultCharset(), false); + tokenRequest.content(new StringContentProvider(tokenEncoded)); + ContentResponse tokenResponse = tokenRequest.timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS).send(); + AuthResponse authResponseJson = Converter.getGson().fromJson(tokenResponse.getContentAsString(), + AuthResponse.class); + token.setToken(authResponseJson.accessToken); + token.setType(authResponseJson.tokenType); + token.setExpiration(authResponseJson.expiresIn); + token.setMyBmwApiUsage(true); + return true; + } + } catch (InterruptedException | ExecutionException | + + TimeoutException e) { + logger.debug("Authorization exception: {}", e.getMessage()); + } + return false; + } + + private boolean startAuthClient() { + if (!authHttpClient.isStarted()) { + try { + authHttpClient.start(); + } catch (Exception e) { + logger.error("Auth HttpClient start failed!"); + return false; + } + } + return true; + } + + private MultiMap getTokenBaseValues() { MultiMap baseValues = new MultiMap(); baseValues.add(CLIENT_ID, Constants.EMPTY + BimmerConstants.CLIENT_ID.get(configuration.region)); baseValues.add(RESPONSE_TYPE, CODE); @@ -303,69 +349,36 @@ public synchronized boolean updateToken(HttpClient client) { baseValues.add("state", Constants.EMPTY + BimmerConstants.STATE.get(configuration.region)); baseValues.add("nonce", "login_nonce"); baseValues.add(SCOPE, BimmerConstants.SCOPE_VALUES); + return baseValues; + } + private MultiMap getTokenAuthValues() { MultiMap authValues = new MultiMap(); authValues.add(GRANT_TYPE, "authorization_code"); authValues.add(USERNAME, configuration.userName); authValues.add(PASSWORD, configuration.password); + return authValues; + } - MultiMap authChallenge = new MultiMap(); - authChallenge.addAllValues(baseValues); - authChallenge.addAllValues(authValues); + private MultiMap getTokenValues(String code) { + MultiMap tokenValues = new MultiMap(); + tokenValues.put(CODE, code); + tokenValues.put("code_verifier", Constants.EMPTY + BimmerConstants.CODE_VERIFIER.get(configuration.region)); + tokenValues.put(REDIRECT_URI, BimmerConstants.REDIRECT_URI_VALUE); + tokenValues.put(GRANT_TYPE, "authorization_code"); + return tokenValues; + } - String authEncoded = UrlEncoded.encode(authChallenge, Charset.defaultCharset(), false); - authRequest.content(new StringContentProvider(authEncoded)); - try { - ContentResponse authResponse = authRequest.timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS).send(); - String authResponseString = URLDecoder.decode(authResponse.getContentAsString(), Charset.defaultCharset()); - String[] keys = authResponseString.split("&"); - for (int i = 0; i < keys.length; i++) { - if (keys[i].startsWith(AUTHORIZATION)) { - String authCode = keys[i].split("=")[1]; - authCode = authCode.split("\"")[0]; - MultiMap codeChallenge = new MultiMap(); - codeChallenge.addAllValues(baseValues); - codeChallenge.put(AUTHORIZATION, authCode); - - Request codeRequest = client.POST(authUri).followRedirects(false); - codeRequest.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED); - // codeRequest.header("User-Agent", "okhttp/3.12.2"); - String codeEncoded = UrlEncoded.encode(codeChallenge, Charset.defaultCharset(), false); - // codeEncoded += "&authorization=" + UrlEncoded.encodeString(authCode); - codeRequest.content(new StringContentProvider(codeEncoded)); - ContentResponse codeResponse = codeRequest.timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS).send(); - String code = ConnectedDriveProxy.codeFromUrl(codeResponse.getHeaders().get(HttpHeader.LOCATION)); - - // Get Token - String tokenUrl = "https://" + BimmerConstants.AUTH_SERVER_MAP.get(configuration.region) - + BimmerConstants.TOKEN_ENDPOINT; - - Request tokenRequest = client.POST(tokenUrl).followRedirects(false); - tokenRequest.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_URL_ENCODED); - tokenRequest.header(HttpHeader.AUTHORIZATION, - BimmerConstants.AUTHORIZATION_VALUE_MAP.get(configuration.region)); - MultiMap tokenValues = new MultiMap(); - tokenValues.put(CODE, code); - tokenValues.put("code_verifier", - Constants.EMPTY + BimmerConstants.CODE_VERIFIER.get(configuration.region)); - tokenValues.put(REDIRECT_URI, BimmerConstants.REDIRECT_URI_VALUE); - tokenValues.put(GRANT_TYPE, "authorization_code"); - String tokenEncoded = UrlEncoded.encode(tokenValues, Charset.defaultCharset(), false); - tokenRequest.content(new StringContentProvider(tokenEncoded)); - ContentResponse tokenResponse = tokenRequest.timeout(HTTP_TIMEOUT_SEC, TimeUnit.SECONDS).send(); - AuthResponse authResponseJson = Converter.getGson().fromJson(tokenResponse.getContentAsString(), - AuthResponse.class); - token.setToken(authResponseJson.accessToken); - token.setType(authResponseJson.tokenType); - token.setExpiration(authResponseJson.expiresIn); - token.setMyBmwApiUsage(true); - return true; - } + private String getAuthCode(String response) { + String[] keys = response.split("&"); + for (int i = 0; i < keys.length; i++) { + if (keys[i].startsWith(AUTHORIZATION)) { + String authCode = keys[i].split("=")[1]; + authCode = authCode.split("\"")[0]; + return authCode; } - } catch (InterruptedException | ExecutionException | TimeoutException e) { - logger.debug("Authorization exception: {}", e.getMessage()); } - return false; + return Constants.EMPTY; } public synchronized boolean updateLegacyToken() { @@ -410,7 +423,6 @@ public synchronized boolean updateLegacyToken() { } public boolean tokenFromUrl(String encodedUrl) { - final StringBuilder result = new StringBuilder(); final MultiMap tokenMap = new MultiMap(); UrlEncoded.decodeTo(encodedUrl, tokenMap, StandardCharsets.US_ASCII); tokenMap.forEach((key, value) -> { @@ -418,7 +430,6 @@ public boolean tokenFromUrl(String encodedUrl) { String val = value.get(0); if (key.endsWith(ACCESS_TOKEN)) { token.setToken(val.toString()); - result.append(true); } else if (key.equals(EXPIRES_IN)) { token.setExpiration(Integer.parseInt(val.toString())); } else if (key.equals(TOKEN_TYPE)) { @@ -426,7 +437,8 @@ public boolean tokenFromUrl(String encodedUrl) { } } }); - return Boolean.valueOf(result.toString()); + logger.info("Token valid? {}", token.isValid()); + return token.isValid(); } public static String codeFromUrl(String encodedUrl) { @@ -446,13 +458,12 @@ public static String codeFromUrl(String encodedUrl) { private String getAuthEncodedData() { MultiMap dataMap = new MultiMap(); - dataMap.add(CLIENT_ID, clientId); + dataMap.add(CLIENT_ID, BimmerConstants.LEGACY_CLIENT_ID); dataMap.add(RESPONSE_TYPE, TOKEN); dataMap.add(REDIRECT_URI, BimmerConstants.LEGACY_REDIRECT_URI_VALUE); dataMap.add(SCOPE, BimmerConstants.LEGACY_SCOPE_VALUES); dataMap.add(USERNAME, configuration.userName); dataMap.add(PASSWORD, configuration.password); - // return UrlEncoded.encode(dataMap, StandardCharsets.UTF_8, false); return UrlEncoded.encode(dataMap, Charset.defaultCharset(), false); } } diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/RemoteServiceHandler.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/RemoteServiceHandler.java index 86c619ccafc93..60aa4aa432d9e 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/RemoteServiceHandler.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/RemoteServiceHandler.java @@ -77,22 +77,23 @@ public enum ExecutionState { } public enum RemoteService { - LIGHT_FLASH(REMOTE_SERVICE_LIGHT_FLASH, "Flash Lights"), - VEHICLE_FINDER(REMOTE_SERVICE_VEHICLE_FINDER, "Vehicle Finder"), - DOOR_LOCK(REMOTE_SERVICE_DOOR_LOCK, "Door Lock"), - DOOR_UNLOCK(REMOTE_SERVICE_DOOR_UNLOCK, "Door Unlock"), - HORN_BLOW(REMOTE_SERVICE_HORN, "Horn Blow"), - CLIMATE_NOW(REMOTE_SERVICE_AIR_CONDITIONING, "Climate Control"), - CHARGE_NOW(REMOTE_SERVICE_CHARGE_NOW, "Start Charging"), - CHARGING_CONTROL(REMOTE_SERVICE_CHARGING_CONTROL, "Send Charging Profile"), - CHARGING_PROFILE("chargingProfile", "Send Charging Profile"); + LIGHT_FLASH(REMOTE_SERVICE_LIGHT_FLASH, "Flash Lights", "light-flash"), + VEHICLE_FINDER(REMOTE_SERVICE_VEHICLE_FINDER, "Vehicle Finder", "vehicle-finder"), + DOOR_LOCK(REMOTE_SERVICE_DOOR_LOCK, "Door Lock", "door-lock"), + DOOR_UNLOCK(REMOTE_SERVICE_DOOR_UNLOCK, "Door Unlock", "door-unlock"), + HORN_BLOW(REMOTE_SERVICE_HORN, "Horn Blow", "horn-blow"), + CLIMATE_NOW(REMOTE_SERVICE_AIR_CONDITIONING, "Climate Control", "air-conditioning"), + CHARGE_NOW(REMOTE_SERVICE_CHARGE_NOW, "Start Charging", "charge-now"), + CHARGING_CONTROL(REMOTE_SERVICE_CHARGING_CONTROL, "Send Charging Profile", "charging-control"); private final String command; private final String label; + private final String remoteCommand; - RemoteService(final String command, final String label) { + RemoteService(final String command, final String label, final String remoteCommand) { this.command = command; this.label = label; + this.remoteCommand = remoteCommand; } public String getCommand() { @@ -102,6 +103,10 @@ public String getCommand() { public String getLabel() { return label; } + + public String getRemoteCommand() { + return remoteCommand; + } } public RemoteServiceHandler(VehicleHandler vehicleHandler, ConnectedDriveProxy connectedDriveProxy) { @@ -127,10 +132,10 @@ boolean execute(RemoteService service, String... data) { final MultiMap dataMap = new MultiMap(); if (data.length > 0) { dataMap.add(DATA, data[0]); - proxy.post(serviceExecutionAPI + service.name().toLowerCase().replace("_", "-"), - CONTENT_TYPE_JSON_ENCODED, "{CHARGING_PROFILE:" + data[0] + "}", this); + proxy.post(serviceExecutionAPI + service.getRemoteCommand(), CONTENT_TYPE_JSON_ENCODED, + "{CHARGING_PROFILE:" + data[0] + "}", this); } else { - proxy.post(serviceExecutionAPI + service.name().toLowerCase().replace("_", "-"), null, null, this); + proxy.post(serviceExecutionAPI + service.getRemoteCommand(), null, null, this); } } else { final MultiMap dataMap = new MultiMap(); diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/Token.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/Token.java index a1dd9cb452f86..f8645197757c1 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/Token.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/Token.java @@ -47,19 +47,13 @@ public void setExpiration(int expiration) { this.expiration = System.currentTimeMillis() / 1000 + expiration; } - /** - * @return true if Token expires in less than 1 second - */ - public boolean isExpired() { - return (expiration - System.currentTimeMillis() / 1000) < 1; - } - public void setType(String type) { tokenType = type; } public boolean isValid() { - return (!token.equals(Constants.EMPTY) && !tokenType.equals(Constants.EMPTY) && expiration > 0); + return (!token.equals(Constants.EMPTY) && !tokenType.equals(Constants.EMPTY) + && (this.expiration - System.currentTimeMillis() / 1000) > 1); } @Override diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java index e6374d93cb38b..d9e9cb3571643 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/handler/VehicleHandler.java @@ -683,8 +683,7 @@ public void onResponse(@Nullable String content) { @Override public void onError(NetworkError error) { - logger.debug("{}", error.toString()); - // vehicleStatusCallback.onError(error); + vehicleStatusCallback.onError(error); } } diff --git a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/BimmerConstants.java b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/BimmerConstants.java index 41ee3f057f45f..0d202922fce03 100644 --- a/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/BimmerConstants.java +++ b/bundles/org.openhab.binding.bmwconnecteddrive/src/main/java/org/openhab/binding/bmwconnecteddrive/internal/utils/BimmerConstants.java @@ -68,9 +68,8 @@ public class BimmerConstants { public static final String LEGACY_CREDENTIAL_VALUES = "nQv6CqtxJuXWP74xf3CJwUEP:1zDHx6un4cDjybLENN3kyfumX2kEYigWPcQpdvDRpIBk7rOJ"; public static final String LEGACY_REDIRECT_URI_VALUE = "https://www.bmw-connecteddrive.com/app/static/external-dispatch.html"; public static final String LEGACY_SCOPE_VALUES = "authenticate_user vehicle_data remote_services"; + public static final String LEGACY_CLIENT_ID = "dbf0a542-ebd1-4ff0-a9a7-55172fbfce35"; - // public static final String LEGACY_CREDENTIAL_VALUES = - // "nQv6CqtxJuXWP74xf3CJwUEP:1zDHx6un4cDjybLENN3kyfumX2kEYigWPcQpdvDRpIBk7rOJ"; public static final String LEGACY_REFERER_URL = "https://www.bmw-connecteddrive.de/app/index.html"; public static final String AUTH_SERVER_NORTH_AMERICA = "login.bmwusa.com/gcdm";