mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 08:47:57 +00:00
Remove BOM integration because it uses webscraping (#41941)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
ee1b6d3195
commit
39ba0fc7ee
@ -98,9 +98,6 @@ omit =
|
||||
homeassistant/components/bme680/sensor.py
|
||||
homeassistant/components/bmp280/sensor.py
|
||||
homeassistant/components/bmw_connected_drive/*
|
||||
homeassistant/components/bom/camera.py
|
||||
homeassistant/components/bom/sensor.py
|
||||
homeassistant/components/bom/weather.py
|
||||
homeassistant/components/braviatv/__init__.py
|
||||
homeassistant/components/braviatv/const.py
|
||||
homeassistant/components/braviatv/media_player.py
|
||||
|
@ -60,7 +60,6 @@ homeassistant/components/blebox/* @gadgetmobile
|
||||
homeassistant/components/blink/* @fronzbot
|
||||
homeassistant/components/bmp280/* @belidzs
|
||||
homeassistant/components/bmw_connected_drive/* @gerard33 @rikroe
|
||||
homeassistant/components/bom/* @maddenp
|
||||
homeassistant/components/bond/* @prystupa
|
||||
homeassistant/components/braviatv/* @bieniu
|
||||
homeassistant/components/broadlink/* @danielhiversen @felipediel
|
||||
|
@ -1 +0,0 @@
|
||||
"""The bom component."""
|
@ -1,132 +0,0 @@
|
||||
"""Provide animated GIF loops of BOM radar imagery."""
|
||||
from bomradarloop import BOMRadarLoop
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
|
||||
from homeassistant.const import CONF_ID, CONF_NAME
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
CONF_DELTA = "delta"
|
||||
CONF_FRAMES = "frames"
|
||||
CONF_LOCATION = "location"
|
||||
CONF_OUTFILE = "filename"
|
||||
|
||||
LOCATIONS = [
|
||||
"Adelaide",
|
||||
"Albany",
|
||||
"AliceSprings",
|
||||
"Bairnsdale",
|
||||
"Bowen",
|
||||
"Brisbane",
|
||||
"Broome",
|
||||
"Cairns",
|
||||
"Canberra",
|
||||
"Carnarvon",
|
||||
"Ceduna",
|
||||
"Dampier",
|
||||
"Darwin",
|
||||
"Emerald",
|
||||
"Esperance",
|
||||
"Geraldton",
|
||||
"Giles",
|
||||
"Gladstone",
|
||||
"Gove",
|
||||
"Grafton",
|
||||
"Gympie",
|
||||
"HallsCreek",
|
||||
"Hobart",
|
||||
"Kalgoorlie",
|
||||
"Katherine",
|
||||
"Learmonth",
|
||||
"Longreach",
|
||||
"Mackay",
|
||||
"Marburg",
|
||||
"Melbourne",
|
||||
"Mildura",
|
||||
"Moree",
|
||||
"MorningtonIs",
|
||||
"MountIsa",
|
||||
"MtGambier",
|
||||
"Namoi",
|
||||
"Newcastle",
|
||||
"Newdegate",
|
||||
"NorfolkIs",
|
||||
"NWTasmania",
|
||||
"Perth",
|
||||
"PortHedland",
|
||||
"Rainbow",
|
||||
"SellicksHill",
|
||||
"SouthDoodlakine",
|
||||
"Sydney",
|
||||
"Townsville",
|
||||
"WaggaWagga",
|
||||
"Warrego",
|
||||
"Warruwi",
|
||||
"Watheroo",
|
||||
"Weipa",
|
||||
"WillisIs",
|
||||
"Wollongong",
|
||||
"Woomera",
|
||||
"Wyndham",
|
||||
"Yarrawonga",
|
||||
]
|
||||
|
||||
|
||||
def _validate_schema(config):
|
||||
if config.get(CONF_LOCATION) is None:
|
||||
if not all(config.get(x) for x in (CONF_ID, CONF_DELTA, CONF_FRAMES)):
|
||||
raise vol.Invalid(
|
||||
f"Specify '{CONF_ID}', '{CONF_DELTA}' and '{CONF_FRAMES}' when '{CONF_LOCATION}' is unspecified"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
LOCATIONS_MSG = f"Set '{CONF_LOCATION}' to one of: {', '.join(sorted(LOCATIONS))}"
|
||||
XOR_MSG = f"Specify exactly one of '{CONF_ID}' or '{CONF_LOCATION}'"
|
||||
|
||||
PLATFORM_SCHEMA = vol.All(
|
||||
PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Exclusive(CONF_ID, "xor", msg=XOR_MSG): cv.string,
|
||||
vol.Exclusive(CONF_LOCATION, "xor", msg=XOR_MSG): vol.In(
|
||||
LOCATIONS, msg=LOCATIONS_MSG
|
||||
),
|
||||
vol.Optional(CONF_DELTA): cv.positive_int,
|
||||
vol.Optional(CONF_FRAMES): cv.positive_int,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_OUTFILE): cv.string,
|
||||
}
|
||||
),
|
||||
_validate_schema,
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up BOM radar-loop camera component."""
|
||||
location = config.get(CONF_LOCATION) or f"ID {config.get(CONF_ID)}"
|
||||
name = config.get(CONF_NAME) or f"BOM Radar Loop - {location}"
|
||||
args = [
|
||||
config.get(x)
|
||||
for x in (CONF_LOCATION, CONF_ID, CONF_DELTA, CONF_FRAMES, CONF_OUTFILE)
|
||||
]
|
||||
add_entities([BOMRadarCam(name, *args)])
|
||||
|
||||
|
||||
class BOMRadarCam(Camera):
|
||||
"""A camera component producing animated BOM radar-imagery GIFs."""
|
||||
|
||||
def __init__(self, name, location, radar_id, delta, frames, outfile):
|
||||
"""Initialize the component."""
|
||||
|
||||
super().__init__()
|
||||
self._name = name
|
||||
self._cam = BOMRadarLoop(location, radar_id, delta, frames, outfile)
|
||||
|
||||
def camera_image(self):
|
||||
"""Return the current BOM radar-loop image."""
|
||||
return self._cam.current
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the component name."""
|
||||
return self._name
|
@ -1,7 +0,0 @@
|
||||
{
|
||||
"domain": "bom",
|
||||
"name": "Australian Bureau of Meteorology (BOM)",
|
||||
"documentation": "https://www.home-assistant.io/integrations/bom",
|
||||
"requirements": ["bomradarloop==0.1.5"],
|
||||
"codeowners": ["@maddenp"]
|
||||
}
|
@ -1,355 +0,0 @@
|
||||
"""Support for Australian BOM (Bureau of Meteorology) weather service."""
|
||||
import datetime
|
||||
import ftplib
|
||||
import gzip
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import zipfile
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION,
|
||||
CONF_LATITUDE,
|
||||
CONF_LONGITUDE,
|
||||
CONF_MONITORED_CONDITIONS,
|
||||
CONF_NAME,
|
||||
LENGTH_KILOMETERS,
|
||||
LENGTH_METERS,
|
||||
LENGTH_MILLIMETERS,
|
||||
PERCENTAGE,
|
||||
PRESSURE_MBAR,
|
||||
SPEED_KILOMETERS_PER_HOUR,
|
||||
TEMP_CELSIUS,
|
||||
)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_LAST_UPDATE = "last_update"
|
||||
ATTR_SENSOR_ID = "sensor_id"
|
||||
ATTR_STATION_ID = "station_id"
|
||||
ATTR_STATION_NAME = "station_name"
|
||||
ATTR_ZONE_ID = "zone_id"
|
||||
|
||||
ATTRIBUTION = "Data provided by the Australian Bureau of Meteorology"
|
||||
|
||||
CONF_STATION = "station"
|
||||
CONF_ZONE_ID = "zone_id"
|
||||
CONF_WMO_ID = "wmo_id"
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(seconds=60)
|
||||
|
||||
SENSOR_TYPES = {
|
||||
"wmo": ["wmo", None],
|
||||
"name": ["Station Name", None],
|
||||
"history_product": ["Zone", None],
|
||||
"local_date_time": ["Local Time", None],
|
||||
"local_date_time_full": ["Local Time Full", None],
|
||||
"aifstime_utc": ["UTC Time Full", None],
|
||||
"lat": ["Lat", None],
|
||||
"lon": ["Long", None],
|
||||
"apparent_t": ["Feels Like C", TEMP_CELSIUS],
|
||||
"cloud": ["Cloud", None],
|
||||
"cloud_base_m": ["Cloud Base", None],
|
||||
"cloud_oktas": ["Cloud Oktas", None],
|
||||
"cloud_type_id": ["Cloud Type ID", None],
|
||||
"cloud_type": ["Cloud Type", None],
|
||||
"delta_t": ["Delta Temp C", TEMP_CELSIUS],
|
||||
"gust_kmh": ["Wind Gust kmh", SPEED_KILOMETERS_PER_HOUR],
|
||||
"gust_kt": ["Wind Gust kt", "kt"],
|
||||
"air_temp": ["Air Temp C", TEMP_CELSIUS],
|
||||
"dewpt": ["Dew Point C", TEMP_CELSIUS],
|
||||
"press": ["Pressure mb", PRESSURE_MBAR],
|
||||
"press_qnh": ["Pressure qnh", "qnh"],
|
||||
"press_msl": ["Pressure msl", "msl"],
|
||||
"press_tend": ["Pressure Tend", None],
|
||||
"rain_trace": ["Rain Today", LENGTH_MILLIMETERS],
|
||||
"rel_hum": ["Relative Humidity", PERCENTAGE],
|
||||
"sea_state": ["Sea State", None],
|
||||
"swell_dir_worded": ["Swell Direction", None],
|
||||
"swell_height": ["Swell Height", LENGTH_METERS],
|
||||
"swell_period": ["Swell Period", None],
|
||||
"vis_km": [f"Visability {LENGTH_KILOMETERS}", LENGTH_KILOMETERS],
|
||||
"weather": ["Weather", None],
|
||||
"wind_dir": ["Wind Direction", None],
|
||||
"wind_spd_kmh": ["Wind Speed kmh", SPEED_KILOMETERS_PER_HOUR],
|
||||
"wind_spd_kt": ["Wind Speed kt", "kt"],
|
||||
}
|
||||
|
||||
|
||||
def validate_station(station):
|
||||
"""Check that the station ID is well-formed."""
|
||||
if station is None:
|
||||
return
|
||||
station = station.replace(".shtml", "")
|
||||
if not re.fullmatch(r"ID[A-Z]\d\d\d\d\d\.\d\d\d\d\d", station):
|
||||
raise vol.error.Invalid("Malformed station ID")
|
||||
return station
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Inclusive(CONF_ZONE_ID, "Deprecated partial station ID"): cv.string,
|
||||
vol.Inclusive(CONF_WMO_ID, "Deprecated partial station ID"): cv.string,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_STATION): validate_station,
|
||||
vol.Required(CONF_MONITORED_CONDITIONS, default=[]): vol.All(
|
||||
cv.ensure_list, [vol.In(SENSOR_TYPES)]
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the BOM sensor."""
|
||||
station = config.get(CONF_STATION)
|
||||
zone_id, wmo_id = config.get(CONF_ZONE_ID), config.get(CONF_WMO_ID)
|
||||
|
||||
if station is not None:
|
||||
if zone_id and wmo_id:
|
||||
_LOGGER.warning(
|
||||
"Using configuration %s, not %s and %s for BOM sensor",
|
||||
CONF_STATION,
|
||||
CONF_ZONE_ID,
|
||||
CONF_WMO_ID,
|
||||
)
|
||||
elif zone_id and wmo_id:
|
||||
station = f"{zone_id}.{wmo_id}"
|
||||
else:
|
||||
station = closest_station(
|
||||
config.get(CONF_LATITUDE),
|
||||
config.get(CONF_LONGITUDE),
|
||||
hass.config.config_dir,
|
||||
)
|
||||
if station is None:
|
||||
_LOGGER.error("Could not get BOM weather station from lat/lon")
|
||||
return
|
||||
|
||||
bom_data = BOMCurrentData(station)
|
||||
|
||||
try:
|
||||
bom_data.update()
|
||||
except ValueError as err:
|
||||
_LOGGER.error("Received error from BOM Current: %s", err)
|
||||
return
|
||||
|
||||
add_entities(
|
||||
[
|
||||
BOMCurrentSensor(bom_data, variable, config.get(CONF_NAME))
|
||||
for variable in config[CONF_MONITORED_CONDITIONS]
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class BOMCurrentSensor(Entity):
|
||||
"""Implementation of a BOM current sensor."""
|
||||
|
||||
def __init__(self, bom_data, condition, stationname):
|
||||
"""Initialize the sensor."""
|
||||
self.bom_data = bom_data
|
||||
self._condition = condition
|
||||
self.stationname = stationname
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
if self.stationname is None:
|
||||
return f"BOM {SENSOR_TYPES[self._condition][0]}"
|
||||
|
||||
return f"BOM {self.stationname} {SENSOR_TYPES[self._condition][0]}"
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self.bom_data.get_reading(self._condition)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the device."""
|
||||
return {
|
||||
ATTR_ATTRIBUTION: ATTRIBUTION,
|
||||
ATTR_LAST_UPDATE: self.bom_data.last_updated,
|
||||
ATTR_SENSOR_ID: self._condition,
|
||||
ATTR_STATION_ID: self.bom_data.latest_data["wmo"],
|
||||
ATTR_STATION_NAME: self.bom_data.latest_data["name"],
|
||||
ATTR_ZONE_ID: self.bom_data.latest_data["history_product"],
|
||||
}
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the units of measurement."""
|
||||
return SENSOR_TYPES[self._condition][1]
|
||||
|
||||
def update(self):
|
||||
"""Update current conditions."""
|
||||
self.bom_data.update()
|
||||
|
||||
|
||||
class BOMCurrentData:
|
||||
"""Get data from BOM."""
|
||||
|
||||
def __init__(self, station_id):
|
||||
"""Initialize the data object."""
|
||||
self._zone_id, self._wmo_id = station_id.split(".")
|
||||
self._data = None
|
||||
self.last_updated = None
|
||||
|
||||
def _build_url(self):
|
||||
"""Build the URL for the requests."""
|
||||
url = (
|
||||
f"http://www.bom.gov.au/fwo/{self._zone_id}"
|
||||
f"/{self._zone_id}.{self._wmo_id}.json"
|
||||
)
|
||||
_LOGGER.debug("BOM URL: %s", url)
|
||||
return url
|
||||
|
||||
@property
|
||||
def latest_data(self):
|
||||
"""Return the latest data object."""
|
||||
if self._data:
|
||||
return self._data[0]
|
||||
return None
|
||||
|
||||
def get_reading(self, condition):
|
||||
"""Return the value for the given condition.
|
||||
|
||||
BOM weather publishes condition readings for weather (and a few other
|
||||
conditions) at intervals throughout the day. To avoid a `-` value in
|
||||
the frontend for these conditions, we traverse the historical data
|
||||
for the latest value that is not `-`.
|
||||
|
||||
Iterators are used in this method to avoid iterating needlessly
|
||||
through the entire BOM provided dataset.
|
||||
"""
|
||||
condition_readings = (entry[condition] for entry in self._data)
|
||||
reading = next((x for x in condition_readings if x != "-"), None)
|
||||
|
||||
if isinstance(reading, (int, float)):
|
||||
return round(reading, 2)
|
||||
return reading
|
||||
|
||||
def should_update(self):
|
||||
"""Determine whether an update should occur.
|
||||
|
||||
BOM provides updated data every 30 minutes. We manually define
|
||||
refreshing logic here rather than a throttle to keep updates
|
||||
in lock-step with BOM.
|
||||
|
||||
If 35 minutes has passed since the last BOM data update, then
|
||||
an update should be done.
|
||||
"""
|
||||
if self.last_updated is None:
|
||||
# Never updated before, therefore an update should occur.
|
||||
return True
|
||||
|
||||
now = dt_util.utcnow()
|
||||
update_due_at = self.last_updated + datetime.timedelta(minutes=35)
|
||||
return now > update_due_at
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data from BOM."""
|
||||
if not self.should_update():
|
||||
_LOGGER.debug(
|
||||
"BOM was updated %s minutes ago, skipping update as"
|
||||
" < 35 minutes, Now: %s, LastUpdate: %s",
|
||||
(dt_util.utcnow() - self.last_updated),
|
||||
dt_util.utcnow(),
|
||||
self.last_updated,
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
result = requests.get(self._build_url(), timeout=10).json()
|
||||
self._data = result["observations"]["data"]
|
||||
|
||||
# set lastupdate using self._data[0] as the first element in the
|
||||
# array is the latest date in the json
|
||||
self.last_updated = dt_util.as_utc(
|
||||
datetime.datetime.strptime(
|
||||
str(self._data[0]["local_date_time_full"]), "%Y%m%d%H%M%S"
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
except ValueError as err:
|
||||
_LOGGER.error("Check BOM %s", err.args)
|
||||
self._data = None
|
||||
raise
|
||||
|
||||
|
||||
def _get_bom_stations():
|
||||
"""Return {CONF_STATION: (lat, lon)} for all stations, for auto-config.
|
||||
|
||||
This function does several MB of internet requests, so please use the
|
||||
caching version to minimize latency and hit-count.
|
||||
"""
|
||||
latlon = {}
|
||||
with io.BytesIO() as file_obj:
|
||||
with ftplib.FTP("ftp.bom.gov.au") as ftp:
|
||||
ftp.login()
|
||||
ftp.cwd("anon2/home/ncc/metadata/sitelists")
|
||||
ftp.retrbinary("RETR stations.zip", file_obj.write)
|
||||
file_obj.seek(0)
|
||||
with zipfile.ZipFile(file_obj) as zipped:
|
||||
with zipped.open("stations.txt") as station_txt:
|
||||
for _ in range(4):
|
||||
station_txt.readline() # skip header
|
||||
while True:
|
||||
line = station_txt.readline().decode().strip()
|
||||
if len(line) < 120:
|
||||
break # end while loop, ignoring any footer text
|
||||
wmo, lat, lon = (
|
||||
line[a:b].strip() for a, b in [(128, 134), (70, 78), (79, 88)]
|
||||
)
|
||||
if wmo != "..":
|
||||
latlon[wmo] = (float(lat), float(lon))
|
||||
zones = {}
|
||||
pattern = (
|
||||
r'<a href="/products/(?P<zone>ID[A-Z]\d\d\d\d\d)/'
|
||||
r'(?P=zone)\.(?P<wmo>\d\d\d\d\d).shtml">'
|
||||
)
|
||||
for state in ("nsw", "vic", "qld", "wa", "tas", "nt"):
|
||||
url = f"http://www.bom.gov.au/{state}/observations/{state}all.shtml"
|
||||
for zone_id, wmo_id in re.findall(pattern, requests.get(url).text):
|
||||
zones[wmo_id] = zone_id
|
||||
return {f"{zones[k]}.{k}": latlon[k] for k in set(latlon) & set(zones)}
|
||||
|
||||
|
||||
def bom_stations(cache_dir):
|
||||
"""Return {CONF_STATION: (lat, lon)} for all stations, for auto-config.
|
||||
|
||||
Results from internet requests are cached as compressed JSON, making
|
||||
subsequent calls very much faster.
|
||||
"""
|
||||
cache_file = os.path.join(cache_dir, ".bom-stations.json.gz")
|
||||
if not os.path.isfile(cache_file):
|
||||
stations = _get_bom_stations()
|
||||
with gzip.open(cache_file, "wt") as cache:
|
||||
json.dump(stations, cache, sort_keys=True)
|
||||
return stations
|
||||
with gzip.open(cache_file, "rt") as cache:
|
||||
return {k: tuple(v) for k, v in json.load(cache).items()}
|
||||
|
||||
|
||||
def closest_station(lat, lon, cache_dir):
|
||||
"""Return the ZONE_ID.WMO_ID of the closest station to our lat/lon."""
|
||||
if lat is None or lon is None or not os.path.isdir(cache_dir):
|
||||
return
|
||||
stations = bom_stations(cache_dir)
|
||||
|
||||
def comparable_dist(wmo_id):
|
||||
"""Create a psudeo-distance from latitude/longitude."""
|
||||
station_lat, station_lon = stations[wmo_id]
|
||||
return (lat - station_lat) ** 2 + (lon - station_lon) ** 2
|
||||
|
||||
return min(stations, key=comparable_dist)
|
@ -1,115 +0,0 @@
|
||||
"""Support for Australian BOM (Bureau of Meteorology) weather service."""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.weather import PLATFORM_SCHEMA, WeatherEntity
|
||||
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
# Reuse data and API logic from the sensor implementation
|
||||
from .sensor import CONF_STATION, BOMCurrentData, closest_station, validate_station
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_STATION): validate_station}
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the BOM weather platform."""
|
||||
station = config.get(CONF_STATION) or closest_station(
|
||||
config.get(CONF_LATITUDE), config.get(CONF_LONGITUDE), hass.config.config_dir
|
||||
)
|
||||
if station is None:
|
||||
_LOGGER.error("Could not get BOM weather station from lat/lon")
|
||||
return False
|
||||
bom_data = BOMCurrentData(station)
|
||||
try:
|
||||
bom_data.update()
|
||||
except ValueError as err:
|
||||
_LOGGER.error("Received error from BOM_Current: %s", err)
|
||||
return False
|
||||
add_entities([BOMWeather(bom_data, config.get(CONF_NAME))], True)
|
||||
|
||||
|
||||
class BOMWeather(WeatherEntity):
|
||||
"""Representation of a weather condition."""
|
||||
|
||||
def __init__(self, bom_data, stationname=None):
|
||||
"""Initialise the platform with a data instance and station name."""
|
||||
self.bom_data = bom_data
|
||||
self.stationname = stationname or self.bom_data.latest_data.get("name")
|
||||
|
||||
def update(self):
|
||||
"""Update current conditions."""
|
||||
self.bom_data.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return f"BOM {self.stationname or '(unknown station)'}"
|
||||
|
||||
@property
|
||||
def condition(self):
|
||||
"""Return the current condition."""
|
||||
return self.bom_data.get_reading("weather") or self.bom_data.get_reading(
|
||||
"cloud"
|
||||
)
|
||||
|
||||
# Now implement the WeatherEntity interface
|
||||
|
||||
@property
|
||||
def temperature(self):
|
||||
"""Return the platform temperature."""
|
||||
return self.bom_data.get_reading("air_temp")
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def pressure(self):
|
||||
"""Return the mean sea-level pressure."""
|
||||
return self.bom_data.get_reading("press_msl")
|
||||
|
||||
@property
|
||||
def humidity(self):
|
||||
"""Return the relative humidity."""
|
||||
return self.bom_data.get_reading("rel_hum")
|
||||
|
||||
@property
|
||||
def wind_speed(self):
|
||||
"""Return the wind speed."""
|
||||
return self.bom_data.get_reading("wind_spd_kmh")
|
||||
|
||||
@property
|
||||
def wind_bearing(self):
|
||||
"""Return the wind bearing."""
|
||||
directions = [
|
||||
"N",
|
||||
"NNE",
|
||||
"NE",
|
||||
"ENE",
|
||||
"E",
|
||||
"ESE",
|
||||
"SE",
|
||||
"SSE",
|
||||
"S",
|
||||
"SSW",
|
||||
"SW",
|
||||
"WSW",
|
||||
"W",
|
||||
"WNW",
|
||||
"NW",
|
||||
"NNW",
|
||||
]
|
||||
wind = {name: idx * 360 / 16 for idx, name in enumerate(directions)}
|
||||
return wind.get(self.bom_data.get_reading("wind_dir"))
|
||||
|
||||
@property
|
||||
def attribution(self):
|
||||
"""Return the attribution."""
|
||||
return "Data provided by the Australian Bureau of Meteorology"
|
@ -369,9 +369,6 @@ blockchain==1.4.4
|
||||
# homeassistant.components.bme680
|
||||
# bme680==1.0.5
|
||||
|
||||
# homeassistant.components.bom
|
||||
bomradarloop==0.1.5
|
||||
|
||||
# homeassistant.components.bond
|
||||
bond-api==0.1.8
|
||||
|
||||
|
@ -197,9 +197,6 @@ blebox_uniapi==1.3.2
|
||||
# homeassistant.components.blink
|
||||
blinkpy==0.16.3
|
||||
|
||||
# homeassistant.components.bom
|
||||
bomradarloop==0.1.5
|
||||
|
||||
# homeassistant.components.bond
|
||||
bond-api==0.1.8
|
||||
|
||||
|
@ -1 +0,0 @@
|
||||
"""Tests for the bom component."""
|
@ -1,104 +0,0 @@
|
||||
"""The tests for the BOM Weather sensor platform."""
|
||||
import json
|
||||
import re
|
||||
import unittest
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.components import sensor
|
||||
from homeassistant.components.bom.sensor import BOMCurrentData
|
||||
from homeassistant.setup import setup_component
|
||||
|
||||
from tests.async_mock import patch
|
||||
from tests.common import assert_setup_component, get_test_home_assistant, load_fixture
|
||||
|
||||
VALID_CONFIG = {
|
||||
"platform": "bom",
|
||||
"station": "IDN60901.94767",
|
||||
"name": "Fake",
|
||||
"monitored_conditions": ["apparent_t", "press", "weather"],
|
||||
}
|
||||
|
||||
|
||||
def mocked_requests(*args, **kwargs):
|
||||
"""Mock requests.get invocations."""
|
||||
|
||||
class MockResponse:
|
||||
"""Class to represent a mocked response."""
|
||||
|
||||
def __init__(self, json_data, status_code):
|
||||
"""Initialize the mock response class."""
|
||||
self.json_data = json_data
|
||||
self.status_code = status_code
|
||||
|
||||
def json(self):
|
||||
"""Return the json of the response."""
|
||||
return self.json_data
|
||||
|
||||
@property
|
||||
def content(self):
|
||||
"""Return the content of the response."""
|
||||
return self.json()
|
||||
|
||||
def raise_for_status(self):
|
||||
"""Raise an HTTPError if status is not 200."""
|
||||
if self.status_code != 200:
|
||||
raise requests.HTTPError(self.status_code)
|
||||
|
||||
url = urlparse(args[0])
|
||||
if re.match(r"^/fwo/[\w]+/[\w.]+\.json", url.path):
|
||||
return MockResponse(json.loads(load_fixture("bom_weather.json")), 200)
|
||||
|
||||
raise NotImplementedError(f"Unknown route {url.path}")
|
||||
|
||||
|
||||
class TestBOMWeatherSensor(unittest.TestCase):
|
||||
"""Test the BOM Weather sensor."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up things to be run when tests are started."""
|
||||
self.hass = get_test_home_assistant()
|
||||
self.config = VALID_CONFIG
|
||||
self.addCleanup(self.hass.stop)
|
||||
|
||||
@patch("requests.get", side_effect=mocked_requests)
|
||||
def test_setup(self, mock_get):
|
||||
"""Test the setup with custom settings."""
|
||||
with assert_setup_component(1, sensor.DOMAIN):
|
||||
assert setup_component(self.hass, sensor.DOMAIN, {"sensor": VALID_CONFIG})
|
||||
self.hass.block_till_done()
|
||||
|
||||
fake_entities = [
|
||||
"bom_fake_feels_like_c",
|
||||
"bom_fake_pressure_mb",
|
||||
"bom_fake_weather",
|
||||
]
|
||||
|
||||
for entity_id in fake_entities:
|
||||
state = self.hass.states.get(f"sensor.{entity_id}")
|
||||
assert state is not None
|
||||
|
||||
@patch("requests.get", side_effect=mocked_requests)
|
||||
def test_sensor_values(self, mock_get):
|
||||
"""Test retrieval of sensor values."""
|
||||
assert setup_component(self.hass, sensor.DOMAIN, {"sensor": VALID_CONFIG})
|
||||
self.hass.block_till_done()
|
||||
|
||||
weather = self.hass.states.get("sensor.bom_fake_weather").state
|
||||
assert "Fine" == weather
|
||||
|
||||
pressure = self.hass.states.get("sensor.bom_fake_pressure_mb").state
|
||||
assert "1021.7" == pressure
|
||||
|
||||
feels_like = self.hass.states.get("sensor.bom_fake_feels_like_c").state
|
||||
assert "25.0" == feels_like
|
||||
|
||||
|
||||
class TestBOMCurrentData(unittest.TestCase):
|
||||
"""Test the BOM data container."""
|
||||
|
||||
def test_should_update_initial(self):
|
||||
"""Test that the first update always occurs."""
|
||||
bom_data = BOMCurrentData("IDN60901.94767")
|
||||
assert bom_data.should_update() is True
|
Loading…
x
Reference in New Issue
Block a user