Add AccuWeather integration (#37166)

* Initial commit

* Fix strings

* Fix unit system

* Add config_flow tests

* Simplify tests

* More tests

* Update comment

* Fix pylint error

* Run gen_requirements_all

* Fix pyline error

* Round precipitation and precipitation probability

* Bump backend library

* Bump backend library

* Add undo update listener on unload

* Add translation key for invalid_api_key

* Remove entity_registry_enabled_default property

* Suggested change

* Bump library
This commit is contained in:
Maciej Bieniek 2020-07-24 22:59:15 +02:00 committed by GitHub
parent 9fe142a114
commit 581c4a4edd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 998 additions and 0 deletions

View File

@ -8,6 +8,9 @@ omit =
homeassistant/scripts/*.py
# omit pieces of code that rely on external devices being present
homeassistant/components/accuweather/__init__.py
homeassistant/components/accuweather/const.py
homeassistant/components/accuweather/weather.py
homeassistant/components/acer_projector/switch.py
homeassistant/components/actiontec/device_tracker.py
homeassistant/components/acmeda/__init__.py

View File

@ -14,6 +14,7 @@ homeassistant/scripts/check_config.py @kellerza
# Integrations
homeassistant/components/abode/* @shred86
homeassistant/components/accuweather/* @bieniu
homeassistant/components/acmeda/* @atmurray
homeassistant/components/adguard/* @frenck
homeassistant/components/agent_dvr/* @ispysoftware

View File

@ -0,0 +1,132 @@
"""The AccuWeather component."""
import asyncio
from datetime import timedelta
import logging
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
from aiohttp.client_exceptions import ClientConnectorError
from async_timeout import timeout
from homeassistant.const import CONF_API_KEY
from homeassistant.core import Config, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import (
ATTR_FORECAST,
CONF_FORECAST,
COORDINATOR,
DOMAIN,
UNDO_UPDATE_LISTENER,
)
_LOGGER = logging.getLogger(__name__)
PLATFORMS = ["weather"]
async def async_setup(hass: HomeAssistant, config: Config) -> bool:
"""Set up configured AccuWeather."""
hass.data.setdefault(DOMAIN, {})
return True
async def async_setup_entry(hass, config_entry) -> bool:
"""Set up AccuWeather as config entry."""
api_key = config_entry.data[CONF_API_KEY]
location_key = config_entry.unique_id
forecast = config_entry.options.get(CONF_FORECAST, False)
_LOGGER.debug("Using location_key: %s, get forecast: %s", location_key, forecast)
websession = async_get_clientsession(hass)
coordinator = AccuWeatherDataUpdateCoordinator(
hass, websession, api_key, location_key, forecast
)
await coordinator.async_refresh()
if not coordinator.last_update_success:
raise ConfigEntryNotReady
undo_listener = config_entry.add_update_listener(update_listener)
hass.data[DOMAIN][config_entry.entry_id] = {
COORDINATOR: coordinator,
UNDO_UPDATE_LISTENER: undo_listener,
}
for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, component)
)
return True
async def async_unload_entry(hass, config_entry):
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(config_entry, component)
for component in PLATFORMS
]
)
)
hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]()
if unload_ok:
hass.data[DOMAIN].pop(config_entry.entry_id)
return unload_ok
async def update_listener(hass, config_entry):
"""Update listener."""
await hass.config_entries.async_reload(config_entry.entry_id)
class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage fetching AccuWeather data API."""
def __init__(self, hass, session, api_key, location_key, forecast: bool):
"""Initialize."""
self.location_key = location_key
self.forecast = forecast
self.is_metric = hass.config.units.is_metric
self.accuweather = AccuWeather(api_key, session, location_key=self.location_key)
# Enabling the forecast download increases the number of requests per data
# update, we use 32 minutes for current condition only and 64 minutes for
# current condition and forecast as update interval to not exceed allowed number
# of requests. We have 50 requests allowed per day, so we use 45 and leave 5 as
# a reserve for restarting HA.
update_interval = (
timedelta(minutes=64) if self.forecast else timedelta(minutes=32)
)
_LOGGER.debug("Data will be update every %s", update_interval)
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval)
async def _async_update_data(self):
"""Update data via library."""
try:
with timeout(10):
current = await self.accuweather.async_get_current_conditions()
forecast = (
await self.accuweather.async_get_forecast(metric=self.is_metric)
if self.forecast
else {}
)
except (
ApiError,
ClientConnectorError,
InvalidApiKeyError,
RequestsExceededError,
) as error:
raise UpdateFailed(error)
_LOGGER.debug("Requests remaining: %s", self.accuweather.requests_remaining)
return {**current, **{ATTR_FORECAST: forecast}}

View File

@ -0,0 +1,112 @@
"""Adds config flow for AccuWeather."""
import asyncio
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
from aiohttp import ClientError
from aiohttp.client_exceptions import ClientConnectorError
from async_timeout import timeout
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from .const import CONF_FORECAST, DOMAIN # pylint:disable=unused-import
class AccuWeatherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow for AccuWeather."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
# Under the terms of use of the API, one user can use one free API key. Due to
# the small number of requests allowed, we only allow one integration instance.
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
errors = {}
if user_input is not None:
websession = async_get_clientsession(self.hass)
try:
with timeout(10):
accuweather = AccuWeather(
user_input[CONF_API_KEY],
websession,
latitude=user_input[CONF_LATITUDE],
longitude=user_input[CONF_LONGITUDE],
)
await accuweather.async_get_location()
except (ApiError, ClientConnectorError, asyncio.TimeoutError, ClientError):
errors["base"] = "cannot_connect"
except InvalidApiKeyError:
errors[CONF_API_KEY] = "invalid_api_key"
except RequestsExceededError:
errors[CONF_API_KEY] = "requests_exceeded"
else:
await self.async_set_unique_id(
accuweather.location_key, raise_on_progress=False
)
return self.async_create_entry(
title=user_input[CONF_NAME], data=user_input
)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_API_KEY): str,
vol.Optional(
CONF_LATITUDE, default=self.hass.config.latitude
): cv.latitude,
vol.Optional(
CONF_LONGITUDE, default=self.hass.config.longitude
): cv.longitude,
vol.Optional(
CONF_NAME, default=self.hass.config.location_name
): str,
}
),
errors=errors,
)
@staticmethod
@callback
def async_get_options_flow(config_entry):
"""Options callback for AccuWeather."""
return AccuWeatherOptionsFlowHandler(config_entry)
class AccuWeatherOptionsFlowHandler(config_entries.OptionsFlow):
"""Config flow options for AccuWeather."""
def __init__(self, config_entry):
"""Initialize AccuWeather options flow."""
self.config_entry = config_entry
async def async_step_init(self, user_input=None):
"""Manage the options."""
return await self.async_step_user()
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Optional(
CONF_FORECAST,
default=self.config_entry.options.get(CONF_FORECAST, False),
): bool
}
),
)

View File

@ -0,0 +1,23 @@
"""Constants for AccuWeather integration."""
ATTRIBUTION = "Data provided by AccuWeather"
ATTR_FORECAST = CONF_FORECAST = "forecast"
COORDINATOR = "coordinator"
DOMAIN = "accuweather"
UNDO_UPDATE_LISTENER = "undo_update_listener"
CONDITION_CLASSES = {
"clear-night": [33, 34, 37],
"cloudy": [7, 8, 38],
"exceptional": [24, 30, 31],
"fog": [11],
"hail": [25],
"lightning": [15],
"lightning-rainy": [16, 17, 41, 42],
"partlycloudy": [4, 6, 35, 36],
"pouring": [18],
"rainy": [12, 13, 14, 26, 39, 40],
"snowy": [19, 20, 21, 22, 23, 43, 44],
"snowy-rainy": [29],
"sunny": [1, 2, 3, 5],
"windy": [32],
}

View File

@ -0,0 +1,8 @@
{
"domain": "accuweather",
"name": "AccuWeather",
"documentation": "https://github.com/bieniu/ha-accuweather",
"requirements": ["accuweather==0.0.9"],
"codeowners": ["@bieniu"],
"config_flow": true
}

View File

@ -0,0 +1,35 @@
{
"config": {
"step": {
"user": {
"title": "AccuWeather",
"description": "If you need help with the configuration have a look here: https://www.home-assistant.io/integrations/accuweather/\n\nWeather forecast is not enabled by default. You can enable it in the integration options.",
"data": {
"name": "Name of the integration",
"api_key": "[%key:common::config_flow::data::api_key%]",
"latitude": "Latitude",
"longitude": "Longitude"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
"requests_exceeded": "The allowed number of requests to Accuweather API has been exceeded. You have to wait or change API Key."
},
"abort": {
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
},
"options": {
"step": {
"user": {
"title": "AccuWeather Options",
"description": "Due to the limitations of the free version of the AccuWeather API key, when you enable weather forecast, data updates will be performed every 64 minutes instead of every 32 minutes.",
"data": {
"forecast": "Weather forecast"
}
}
}
}
}

View File

@ -0,0 +1,179 @@
"""Support for the AccuWeather service."""
from statistics import mean
from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_PRECIPITATION,
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_FORECAST_TEMP,
ATTR_FORECAST_TEMP_LOW,
ATTR_FORECAST_TIME,
ATTR_FORECAST_WIND_BEARING,
ATTR_FORECAST_WIND_SPEED,
WeatherEntity,
)
from homeassistant.const import CONF_NAME, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.util.dt import utc_from_timestamp
from .const import ATTR_FORECAST, ATTRIBUTION, CONDITION_CLASSES, COORDINATOR, DOMAIN
PARALLEL_UPDATES = 1
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Add a AccuWeather weather entity from a config_entry."""
name = config_entry.data[CONF_NAME]
coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR]
async_add_entities([AccuWeatherEntity(name, coordinator)], False)
class AccuWeatherEntity(WeatherEntity):
"""Define an AccuWeather entity."""
def __init__(self, name, coordinator):
"""Initialize."""
self._name = name
self.coordinator = coordinator
self._attrs = {}
self._unit_system = "Metric" if self.coordinator.is_metric else "Imperial"
@property
def name(self):
"""Return the name."""
return self._name
@property
def attribution(self):
"""Return the attribution."""
return ATTRIBUTION
@property
def unique_id(self):
"""Return a unique_id for this entity."""
return self.coordinator.location_key
@property
def should_poll(self):
"""Return the polling requirement of the entity."""
return False
@property
def available(self):
"""Return True if entity is available."""
return self.coordinator.last_update_success
@property
def condition(self):
"""Return the current condition."""
try:
return [
k
for k, v in CONDITION_CLASSES.items()
if self.coordinator.data["WeatherIcon"] in v
][0]
except IndexError:
return STATE_UNKNOWN
@property
def temperature(self):
"""Return the temperature."""
return self.coordinator.data["Temperature"][self._unit_system]["Value"]
@property
def temperature_unit(self):
"""Return the unit of measurement."""
return TEMP_CELSIUS if self.coordinator.is_metric else TEMP_FAHRENHEIT
@property
def pressure(self):
"""Return the pressure."""
return self.coordinator.data["Pressure"][self._unit_system]["Value"]
@property
def humidity(self):
"""Return the humidity."""
return self.coordinator.data["RelativeHumidity"]
@property
def wind_speed(self):
"""Return the wind speed."""
return self.coordinator.data["Wind"]["Speed"][self._unit_system]["Value"]
@property
def wind_bearing(self):
"""Return the wind bearing."""
return self.coordinator.data["Wind"]["Direction"]["Degrees"]
@property
def visibility(self):
"""Return the visibility."""
return self.coordinator.data["Visibility"][self._unit_system]["Value"]
@property
def ozone(self):
"""Return the ozone level."""
# We only have ozone data for certain locations and only in the forecast data.
if self.coordinator.forecast and self.coordinator.data[ATTR_FORECAST][0].get(
"Ozone"
):
return self.coordinator.data[ATTR_FORECAST][0]["Ozone"]["Value"]
return None
@property
def forecast(self):
"""Return the forecast array."""
if self.coordinator.forecast:
# remap keys from library to keys understood by the weather component
forecast = [
{
ATTR_FORECAST_TIME: utc_from_timestamp(
item["EpochDate"]
).isoformat(),
ATTR_FORECAST_TEMP: item["TemperatureMax"]["Value"],
ATTR_FORECAST_TEMP_LOW: item["TemperatureMin"]["Value"],
ATTR_FORECAST_PRECIPITATION: self._calc_precipitation(item),
ATTR_FORECAST_PRECIPITATION_PROBABILITY: round(
mean(
[
item["PrecipitationProbabilityDay"],
item["PrecipitationProbabilityNight"],
]
)
),
ATTR_FORECAST_WIND_SPEED: item["WindDay"]["Speed"]["Value"],
ATTR_FORECAST_WIND_BEARING: item["WindDay"]["Direction"]["Degrees"],
ATTR_FORECAST_CONDITION: [
k for k, v in CONDITION_CLASSES.items() if item["IconDay"] in v
][0],
}
for item in self.coordinator.data[ATTR_FORECAST]
]
return forecast
return None
async def async_added_to_hass(self):
"""Connect to dispatcher listening for entity data notifications."""
self.async_on_remove(
self.coordinator.async_add_listener(self.async_write_ha_state)
)
async def async_update(self):
"""Update AccuWeather entity."""
await self.coordinator.async_request_refresh()
@staticmethod
def _calc_precipitation(day: dict) -> float:
"""Return sum of the precipitation."""
precip_sum = 0
precip_types = ["Rain", "Snow", "Ice"]
for precip in precip_types:
precip_sum = sum(
[
precip_sum,
day[f"{precip}Day"]["Value"],
day[f"{precip}Night"]["Value"],
]
)
return round(precip_sum, 1)

View File

@ -7,6 +7,7 @@ To update, run python3 -m script.hassfest
FLOWS = [
"abode",
"accuweather",
"acmeda",
"adguard",
"agent_dvr",

View File

@ -102,6 +102,9 @@ YesssSMS==0.4.1
# homeassistant.components.abode
abodepy==0.19.0
# homeassistant.components.accuweather
accuweather==0.0.9
# homeassistant.components.mcp23017
adafruit-blinka==3.9.0

View File

@ -45,6 +45,9 @@ YesssSMS==0.4.1
# homeassistant.components.abode
abodepy==0.19.0
# homeassistant.components.accuweather
accuweather==0.0.9
# homeassistant.components.androidtv
adb-shell[async]==0.2.0

View File

@ -0,0 +1 @@
"""Tests for AccuWeather."""

View File

@ -0,0 +1,158 @@
"""Define tests for the AccuWeather config flow."""
import json
from accuweather import ApiError, InvalidApiKeyError, RequestsExceededError
from homeassistant import data_entry_flow
from homeassistant.components.accuweather.const import CONF_FORECAST, DOMAIN
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from tests.async_mock import patch
from tests.common import MockConfigEntry, load_fixture
VALID_CONFIG = {
CONF_NAME: "abcd",
CONF_API_KEY: "32-character-string-1234567890qw",
CONF_LATITUDE: 55.55,
CONF_LONGITUDE: 122.12,
}
async def test_show_form(hass):
"""Test that the form is served with no input."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == SOURCE_USER
async def test_invalid_api_key_1(hass):
"""Test that errors are shown when API key is invalid."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
data={
CONF_NAME: "abcd",
CONF_API_KEY: "foo",
CONF_LATITUDE: 55.55,
CONF_LONGITUDE: 122.12,
},
)
assert result["errors"] == {CONF_API_KEY: "invalid_api_key"}
async def test_invalid_api_key_2(hass):
"""Test that errors are shown when API key is invalid."""
with patch(
"accuweather.AccuWeather._async_get_data",
side_effect=InvalidApiKeyError("Invalid API key"),
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=VALID_CONFIG,
)
assert result["errors"] == {CONF_API_KEY: "invalid_api_key"}
async def test_api_error(hass):
"""Test API error."""
with patch(
"accuweather.AccuWeather._async_get_data",
side_effect=ApiError("Invalid response from AccuWeather API"),
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=VALID_CONFIG,
)
assert result["errors"] == {"base": "cannot_connect"}
async def test_requests_exceeded_error(hass):
"""Test requests exceeded error."""
with patch(
"accuweather.AccuWeather._async_get_data",
side_effect=RequestsExceededError(
"The allowed number of requests has been exceeded"
),
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=VALID_CONFIG,
)
assert result["errors"] == {CONF_API_KEY: "requests_exceeded"}
async def test_integration_already_exists(hass):
"""Test we only allow a single config flow."""
with patch(
"accuweather.AccuWeather._async_get_data",
return_value=json.loads(load_fixture("accuweather/location_data.json")),
):
MockConfigEntry(
domain=DOMAIN, unique_id="123456", data=VALID_CONFIG,
).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=VALID_CONFIG,
)
assert result["type"] == "abort"
assert result["reason"] == "single_instance_allowed"
async def test_create_entry(hass):
"""Test that the user step works."""
with patch(
"accuweather.AccuWeather._async_get_data",
return_value=json.loads(load_fixture("accuweather/location_data.json")),
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=VALID_CONFIG,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "abcd"
assert result["data"][CONF_NAME] == "abcd"
assert result["data"][CONF_LATITUDE] == 55.55
assert result["data"][CONF_LONGITUDE] == 122.12
assert result["data"][CONF_API_KEY] == "32-character-string-1234567890qw"
async def test_options_flow(hass):
"""Test config flow options."""
config_entry = MockConfigEntry(
domain=DOMAIN, unique_id="123456", data=VALID_CONFIG,
)
config_entry.add_to_hass(hass)
with patch(
"accuweather.AccuWeather._async_get_data",
return_value=json.loads(load_fixture("accuweather/location_data.json")),
), patch(
"accuweather.AccuWeather.async_get_current_conditions",
return_value=json.loads(
load_fixture("accuweather/current_conditions_data.json")
),
):
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
result = await hass.config_entries.options.async_configure(
result["flow_id"], user_input={CONF_FORECAST: True}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert config_entry.options == {CONF_FORECAST: True}

View File

@ -0,0 +1,290 @@
{
"WeatherIcon": 1,
"HasPrecipitation": false,
"PrecipitationType": null,
"Temperature": {
"Metric": {
"Value": 22.6,
"Unit": "C",
"UnitType": 17
},
"Imperial": {
"Value": 73.0,
"Unit": "F",
"UnitType": 18
}
},
"RealFeelTemperature": {
"Metric": {
"Value": 25.1,
"Unit": "C",
"UnitType": 17
},
"Imperial": {
"Value": 77.0,
"Unit": "F",
"UnitType": 18
}
},
"RealFeelTemperatureShade": {
"Metric": {
"Value": 21.1,
"Unit": "C",
"UnitType": 17
},
"Imperial": {
"Value": 70.0,
"Unit": "F",
"UnitType": 18
}
},
"RelativeHumidity": 67,
"IndoorRelativeHumidity": 67,
"DewPoint": {
"Metric": {
"Value": 16.2,
"Unit": "C",
"UnitType": 17
},
"Imperial": {
"Value": 61.0,
"Unit": "F",
"UnitType": 18
}
},
"Wind": {
"Direction": {
"Degrees": 180,
"Localized": "S",
"English": "S"
},
"Speed": {
"Metric": {
"Value": 14.5,
"Unit": "km/h",
"UnitType": 7
},
"Imperial": {
"Value": 9.0,
"Unit": "mi/h",
"UnitType": 9
}
}
},
"WindGust": {
"Speed": {
"Metric": {
"Value": 20.3,
"Unit": "km/h",
"UnitType": 7
},
"Imperial": {
"Value": 12.6,
"Unit": "mi/h",
"UnitType": 9
}
}
},
"UVIndex": 6,
"UVIndexText": "High",
"Visibility": {
"Metric": {
"Value": 16.1,
"Unit": "km",
"UnitType": 6
},
"Imperial": {
"Value": 10.0,
"Unit": "mi",
"UnitType": 2
}
},
"ObstructionsToVisibility": "",
"CloudCover": 10,
"Ceiling": {
"Metric": {
"Value": 3200.0,
"Unit": "m",
"UnitType": 5
},
"Imperial": {
"Value": 10500.0,
"Unit": "ft",
"UnitType": 0
}
},
"Pressure": {
"Metric": {
"Value": 1012.0,
"Unit": "mb",
"UnitType": 14
},
"Imperial": {
"Value": 29.88,
"Unit": "inHg",
"UnitType": 12
}
},
"PressureTendency": {
"LocalizedText": "Falling",
"Code": "F"
},
"Past24HourTemperatureDeparture": {
"Metric": {
"Value": 0.3,
"Unit": "C",
"UnitType": 17
},
"Imperial": {
"Value": 0.0,
"Unit": "F",
"UnitType": 18
}
},
"ApparentTemperature": {
"Metric": {
"Value": 22.8,
"Unit": "C",
"UnitType": 17
},
"Imperial": {
"Value": 73.0,
"Unit": "F",
"UnitType": 18
}
},
"WindChillTemperature": {
"Metric": {
"Value": 22.8,
"Unit": "C",
"UnitType": 17
},
"Imperial": {
"Value": 73.0,
"Unit": "F",
"UnitType": 18
}
},
"WetBulbTemperature": {
"Metric": {
"Value": 18.6,
"Unit": "C",
"UnitType": 17
},
"Imperial": {
"Value": 65.0,
"Unit": "F",
"UnitType": 18
}
},
"Precip1hr": {
"Metric": {
"Value": 0.0,
"Unit": "mm",
"UnitType": 3
},
"Imperial": {
"Value": 0.0,
"Unit": "in",
"UnitType": 1
}
},
"PrecipitationSummary": {
"Precipitation": {
"Metric": {
"Value": 0.0,
"Unit": "mm",
"UnitType": 3
},
"Imperial": {
"Value": 0.0,
"Unit": "in",
"UnitType": 1
}
},
"PastHour": {
"Metric": {
"Value": 0.0,
"Unit": "mm",
"UnitType": 3
},
"Imperial": {
"Value": 0.0,
"Unit": "in",
"UnitType": 1
}
},
"Past3Hours": {
"Metric": {
"Value": 1.3,
"Unit": "mm",
"UnitType": 3
},
"Imperial": {
"Value": 0.05,
"Unit": "in",
"UnitType": 1
}
},
"Past6Hours": {
"Metric": {
"Value": 1.3,
"Unit": "mm",
"UnitType": 3
},
"Imperial": {
"Value": 0.05,
"Unit": "in",
"UnitType": 1
}
},
"Past9Hours": {
"Metric": {
"Value": 2.5,
"Unit": "mm",
"UnitType": 3
},
"Imperial": {
"Value": 0.1,
"Unit": "in",
"UnitType": 1
}
},
"Past12Hours": {
"Metric": {
"Value": 3.8,
"Unit": "mm",
"UnitType": 3
},
"Imperial": {
"Value": 0.15,
"Unit": "in",
"UnitType": 1
}
},
"Past18Hours": {
"Metric": {
"Value": 5.1,
"Unit": "mm",
"UnitType": 3
},
"Imperial": {
"Value": 0.2,
"Unit": "in",
"UnitType": 1
}
},
"Past24Hours": {
"Metric": {
"Value": 7.6,
"Unit": "mm",
"UnitType": 3
},
"Imperial": {
"Value": 0.3,
"Unit": "in",
"UnitType": 1
}
}
}
}

View File

@ -0,0 +1,49 @@
{
"Version": 1,
"Key": "268068",
"Type": "City",
"Rank": 85,
"LocalizedName": "Piątek",
"EnglishName": "Piątek",
"PrimaryPostalCode": "",
"Region": { "ID": "EUR", "LocalizedName": "Europe", "EnglishName": "Europe" },
"Country": { "ID": "PL", "LocalizedName": "Poland", "EnglishName": "Poland" },
"AdministrativeArea": {
"ID": "10",
"LocalizedName": "Łódź",
"EnglishName": "Łódź",
"Level": 1,
"LocalizedType": "Voivodship",
"EnglishType": "Voivodship",
"CountryID": "PL"
},
"TimeZone": {
"Code": "CEST",
"Name": "Europe/Warsaw",
"GmtOffset": 2.0,
"IsDaylightSaving": true,
"NextOffsetChange": "2020-10-25T01:00:00Z"
},
"GeoPosition": {
"Latitude": 52.069,
"Longitude": 19.479,
"Elevation": {
"Metric": { "Value": 94.0, "Unit": "m", "UnitType": 5 },
"Imperial": { "Value": 308.0, "Unit": "ft", "UnitType": 0 }
}
},
"IsAlias": false,
"SupplementalAdminAreas": [
{ "Level": 2, "LocalizedName": "Łęczyca", "EnglishName": "Łęczyca" },
{ "Level": 3, "LocalizedName": "Piątek", "EnglishName": "Piątek" }
],
"DataSets": [
"AirQualityCurrentConditions",
"AirQualityForecasts",
"Alerts",
"ForecastConfidence",
"FutureRadar",
"MinuteCast",
"Radar"
]
}