From b6a32098d1e9b89d166d1be62ead0ecc7e1fdf03 Mon Sep 17 00:00:00 2001 From: Harald Nagel Date: Thu, 4 Feb 2016 20:01:45 +0000 Subject: [PATCH] Add BloomSky weather station support --- .coveragerc | 3 + homeassistant/components/bloomsky.py | 79 +++++++++++++++ homeassistant/components/camera/bloomsky.py | 60 +++++++++++ homeassistant/components/sensor/bloomsky.py | 104 ++++++++++++++++++++ 4 files changed, 246 insertions(+) create mode 100644 homeassistant/components/bloomsky.py create mode 100644 homeassistant/components/camera/bloomsky.py create mode 100644 homeassistant/components/sensor/bloomsky.py diff --git a/.coveragerc b/.coveragerc index 2fdaa9dff5fd..54968d53ed68 100644 --- a/.coveragerc +++ b/.coveragerc @@ -10,6 +10,9 @@ omit = homeassistant/components/arduino.py homeassistant/components/*/arduino.py + homeassistant/components/bloomsky.py + homeassistant/components/*/bloomsky.py + homeassistant/components/insteon_hub.py homeassistant/components/*/insteon_hub.py diff --git a/homeassistant/components/bloomsky.py b/homeassistant/components/bloomsky.py new file mode 100644 index 000000000000..4ec820502b4b --- /dev/null +++ b/homeassistant/components/bloomsky.py @@ -0,0 +1,79 @@ +""" +homeassistant.components.bloomsky +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for BloomSky weather station. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/bloomsky/ +""" +import logging +from datetime import timedelta +import requests +from homeassistant.util import Throttle +from homeassistant.helpers import validate_config +from homeassistant.const import CONF_API_KEY + +DOMAIN = "bloomsky" +BLOOMSKY = None + +_LOGGER = logging.getLogger(__name__) + +# the BloomSky only updates every 5-8 minutes as per the API spec so there's +# no point in polling the API more frequently +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300) + + +# pylint: disable=unused-argument,too-few-public-methods +def setup(hass, config): + """ + Setup BloomSky component. + """ + if not validate_config( + config, + {DOMAIN: [CONF_API_KEY]}, + _LOGGER): + return False + + api_key = config[DOMAIN][CONF_API_KEY] + + global BLOOMSKY + try: + BLOOMSKY = BloomSky(api_key) + except RuntimeError: + return False + + return True + + +class BloomSky(object): + """Handle all communication with the BloomSky API""" + + # API documentation at http://weatherlution.com/bloomsky-api/ + + API_URL = "https://api.bloomsky.com/api/skydata" + + def __init__(self, api_key): + self._api_key = api_key + self.devices = {} + _LOGGER.debug("Initial bloomsky device load...") + self.refresh_devices() + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def refresh_devices(self): + """ + Uses the API to retreive a list of devices associated with an + account along with all the sensors on the device. + """ + _LOGGER.debug("Fetching bloomsky update") + response = requests.get(self.API_URL, + headers={"Authorization": self._api_key}, + timeout=10) + if response.status_code == 401: + raise RuntimeError("Invalid API_KEY") + elif response.status_code != 200: + _LOGGER.error("Invalid HTTP response: %s", response.status_code) + return + # create dictionary keyed off of the device unique id + self.devices.update({ + device["DeviceID"]: device for device in response.json() + }) diff --git a/homeassistant/components/camera/bloomsky.py b/homeassistant/components/camera/bloomsky.py new file mode 100644 index 000000000000..c57338b99768 --- /dev/null +++ b/homeassistant/components/camera/bloomsky.py @@ -0,0 +1,60 @@ +""" +homeassistant.components.sensor.bloomsky +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for BloomSky weather station. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/bloomsky/ +""" +import logging +import requests +import homeassistant.components.bloomsky as bloomsky +from homeassistant.components.camera import Camera + +DEPENDENCIES = ["bloomsky"] + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """ set up access to BloomSky cameras """ + for device in bloomsky.BLOOMSKY.devices.values(): + add_devices_callback([BloomSkyCamera(bloomsky.BLOOMSKY, device)]) + + +class BloomSkyCamera(Camera): + """ Represents the images published from the BloomSky's camera """ + + def __init__(self, bs, device): + """ set up for access to the BloomSky camera images """ + super(BloomSkyCamera, self).__init__() + self._name = device["DeviceName"] + self._id = device["DeviceID"] + self._bloomsky = bs + self._url = "" + self._last_url = "" + # _last_image will store images as they are downloaded so that the + # frequent updates in home-assistant don't keep poking the server + # to download the same image over and over + self._last_image = "" + self._logger = logging.getLogger(__name__) + + def camera_image(self): + """ update the camera's image if it has changed """ + try: + self._url = self._bloomsky.devices[self._id]["Data"]["ImageURL"] + self._bloomsky.refresh_devices() + # if the url hasn't changed then the image hasn't changed + if self._url != self._last_url: + response = requests.get(self._url, timeout=10) + self._last_url = self._url + self._last_image = response.content + except requests.exceptions.RequestException as error: + self._logger.error("Error getting bloomsky image: %s", error) + return None + + return self._last_image + + @property + def name(self): + """ the name of this BloomSky device """ + return self._name diff --git a/homeassistant/components/sensor/bloomsky.py b/homeassistant/components/sensor/bloomsky.py new file mode 100644 index 000000000000..58a1e5a9799c --- /dev/null +++ b/homeassistant/components/sensor/bloomsky.py @@ -0,0 +1,104 @@ +""" +homeassistant.components.sensor.bloomsky +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for BloomSky weather station. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/bloomsky/ +""" +import logging +import homeassistant.components.bloomsky as bloomsky +from homeassistant.helpers.entity import Entity + +DEPENDENCIES = ["bloomsky"] + +# these are the available sensors +SENSOR_TYPES = ["Temperature", + "Humidity", + "Rain", + "Pressure", + "Luminance", + "Night", + "UVIndex"] + +# sensor units - these do not currently align with the API documentation +SENSOR_UNITS = {"Temperature": "°F", + "Humidity": "%", + "Pressure": "inHg", + "Luminance": "cd/m²"} + +# which sensors to format numerically +FORMAT_NUMBERS = ["Temperature", "Pressure"] + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Set up the available BloomSky weather sensors """ + + logger = logging.getLogger(__name__) + + for device_key in bloomsky.BLOOMSKY.devices: + device = bloomsky.BLOOMSKY.devices[device_key] + for variable in config["monitored_conditions"]: + if variable in SENSOR_TYPES: + add_devices([BloomSkySensor(bloomsky.BLOOMSKY, + device, + variable)]) + else: + logger.error("Cannot find definition for device: %s", variable) + + +class BloomSkySensor(Entity): + """ Represents a single sensor in a BloomSky device """ + + def __init__(self, bs, device, sensor_name): + self._bloomsky = bs + self._device_id = device["DeviceID"] + self._client_name = device["DeviceName"] + self._sensor_name = sensor_name + self._state = self.process_state(device) + self._sensor_update = "" + + @property + def name(self): + """ the name of the BloomSky device and this sensor """ + return "{} {}".format(self._client_name, self._sensor_name) + + @property + def state(self): + """ the current state (i.e. value) of this sensor """ + return self._state + + @property + def unit_of_measurement(self): + """ this sensor's units """ + return SENSOR_UNITS.get(self._sensor_name, None) + + def update(self): + """ request an update from the BloomSky API """ + self._bloomsky.refresh_devices() + # TS is a Unix epoch timestamp for the last time the BloomSky servers + # heard from this device. If that value hasn't changed, the value has + # not been updated. + last_ts = self._bloomsky.devices[self._device_id]["Data"]["TS"] + if last_ts != self._sensor_update: + self.process_state(self._bloomsky.devices[self._device_id]) + self._sensor_update = last_ts + + def process_state(self, device): + """ handle the response from the BloomSky API for this sensor""" + data = device["Data"][self._sensor_name] + if self._sensor_name == "Rain": + if data: + self._state = "Raining" + else: + self._state = "Not raining" + elif self._sensor_name == "Night": + if data: + self._state = "Nighttime" + else: + self._state = "Daytime" + elif self._sensor_name in FORMAT_NUMBERS: + self._state = "{0:.2f}".format(data) + else: + self._state = data