diff --git a/homeassistant/components/sensor/london_air.py b/homeassistant/components/sensor/london_air.py new file mode 100644 index 00000000000..7a8ad4087b0 --- /dev/null +++ b/homeassistant/components/sensor/london_air.py @@ -0,0 +1,216 @@ +""" +Sensor for checking the status of London air. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.london_air/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol +import requests + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import STATE_UNKNOWN +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +_LOGGER = logging.getLogger(__name__) + +CONF_LOCATIONS = 'locations' +SCAN_INTERVAL = timedelta(minutes=30) +AUTHORITIES = [ + 'Barking and Dagenham', + 'Bexley', + 'Brent', + 'Camden', + 'City of London', + 'Croydon', + 'Ealing', + 'Enfield', + 'Greenwich', + 'Hackney', + 'Hammersmith and Fulham', + 'Haringey', + 'Harrow', + 'Havering', + 'Hillingdon', + 'Islington', + 'Kensington and Chelsea', + 'Kingston', + 'Lambeth', + 'Lewisham', + 'Merton', + 'Redbridge', + 'Richmond', + 'Southwark', + 'Sutton', + 'Tower Hamlets', + 'Wandsworth', + 'Westminster'] +URL = ('http://api.erg.kcl.ac.uk/AirQuality/Hourly/' + 'MonitoringIndex/GroupName=London/Json') + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_LOCATIONS, default=AUTHORITIES): + vol.All(cv.ensure_list, [vol.In(AUTHORITIES)]), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Tube sensor.""" + data = APIData() + data.update() + sensors = [] + for name in config.get(CONF_LOCATIONS): + sensors.append(AirSensor(name, data)) + + add_devices(sensors, True) + + +class APIData(object): + """Get the latest data for all authorities.""" + + def __init__(self): + """Initialize the AirData object.""" + self.data = None + + # Update only once in scan interval. + @Throttle(SCAN_INTERVAL) + def update(self): + """Get the latest data from TFL.""" + response = requests.get(URL, timeout=10) + if response.status_code != 200: + _LOGGER.warning("Invalid response from API") + else: + self.data = parse_api_response(response.json()) + + +class AirSensor(Entity): + """Single authority air sensor.""" + + ICON = 'mdi:cloud-outline' + + def __init__(self, name, APIdata): + """Initialize the sensor.""" + self._name = name + self._api_data = APIdata + self._site_data = None + self._state = None + self._updated = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def site_data(self): + """Return the dict of sites data.""" + return self._site_data + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self.ICON + + @property + def device_state_attributes(self): + """Return other details about the sensor state.""" + attrs = {} + attrs['updated'] = self._updated + attrs['sites'] = len(self._site_data) + attrs['data'] = self._site_data + return attrs + + def update(self): + """Update the sensor.""" + self._api_data.update() + self._site_data = self._api_data.data[self._name] + self._updated = self._site_data[0]['updated'] + sites_status = [] + for site in self._site_data: + if site['pollutants_status'] != 'no_species_data': + sites_status.append(site['pollutants_status']) + if sites_status: + self._state = max(set(sites_status), key=sites_status.count) + else: + self._state = STATE_UNKNOWN + + +def parse_species(species_data): + """Iterate over list of species at each site.""" + parsed_species_data = [] + quality_list = [] + for species in species_data: + if species['@AirQualityBand'] != 'No data': + species_dict = {} + species_dict['description'] = species['@SpeciesDescription'] + species_dict['code'] = species['@SpeciesCode'] + species_dict['quality'] = species['@AirQualityBand'] + species_dict['index'] = species['@AirQualityIndex'] + species_dict['summary'] = (species_dict['code'] + ' is ' + + species_dict['quality']) + parsed_species_data.append(species_dict) + quality_list.append(species_dict['quality']) + return parsed_species_data, quality_list + + +def parse_site(entry_sites_data): + """Iterate over all sites at an authority.""" + authority_data = [] + for site in entry_sites_data: + site_data = {} + species_data = [] + + site_data['updated'] = site['@BulletinDate'] + site_data['latitude'] = site['@Latitude'] + site_data['longitude'] = site['@Longitude'] + site_data['site_code'] = site['@SiteCode'] + site_data['site_name'] = site['@SiteName'].split("-")[-1].lstrip() + site_data['site_type'] = site['@SiteType'] + + if isinstance(site['Species'], dict): + species_data = [site['Species']] + else: + species_data = site['Species'] + + parsed_species_data, quality_list = parse_species(species_data) + + if not parsed_species_data: + parsed_species_data.append('no_species_data') + site_data['pollutants'] = parsed_species_data + + if quality_list: + site_data['pollutants_status'] = max(set(quality_list), + key=quality_list.count) + site_data['number_of_pollutants'] = len(quality_list) + else: + site_data['pollutants_status'] = 'no_species_data' + site_data['number_of_pollutants'] = 0 + + authority_data.append(site_data) + return authority_data + + +def parse_api_response(response): + """API can return dict or list of data so need to check.""" + data = dict.fromkeys(AUTHORITIES) + for authority in AUTHORITIES: + for entry in response['HourlyAirQualityIndex']['LocalAuthority']: + if entry['@LocalAuthorityName'] == authority: + + if isinstance(entry['Site'], dict): + entry_sites_data = [entry['Site']] + else: + entry_sites_data = entry['Site'] + + data[authority] = parse_site(entry_sites_data) + + return data diff --git a/tests/components/sensor/test_london_air.py b/tests/components/sensor/test_london_air.py new file mode 100644 index 00000000000..56084095bd2 --- /dev/null +++ b/tests/components/sensor/test_london_air.py @@ -0,0 +1,41 @@ +"""The tests for the tube_state platform.""" +import unittest +import requests_mock + +from homeassistant.components.sensor.london_air import ( + CONF_LOCATIONS, URL) +from homeassistant.setup import setup_component +from tests.common import load_fixture, get_test_home_assistant + +VALID_CONFIG = { + 'platform': 'london_air', + CONF_LOCATIONS: [ + 'Merton', + ] +} + + +class TestLondonAirSensor(unittest.TestCase): + """Test the tube_state platform.""" + + def setUp(self): + """Initialize values for this testcase class.""" + self.hass = get_test_home_assistant() + self.config = VALID_CONFIG + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + @requests_mock.Mocker() + def test_setup(self, mock_req): + """Test for operational tube_state sensor with proper attributes.""" + mock_req.get(URL, text=load_fixture('london_air.json')) + self.assertTrue( + setup_component(self.hass, 'sensor', {'sensor': self.config})) + + state = self.hass.states.get('sensor.merton') + assert state.state == 'Low' + assert state.attributes.get('updated') == '2017-08-03 03:00:00' + assert state.attributes.get('sites') == 2 + assert state.attributes.get('data')[0]['site_code'] == 'ME2' diff --git a/tests/fixtures/london_air.json b/tests/fixtures/london_air.json new file mode 100644 index 00000000000..3a3d9afb643 --- /dev/null +++ b/tests/fixtures/london_air.json @@ -0,0 +1,52 @@ +{ + "HourlyAirQualityIndex": { + "@GroupName": "London", + "@TimeToLive": "38", + "LocalAuthority": [ + { + "@LocalAuthorityCode": "24", + "@LocalAuthorityName": "Merton", + "@LaCentreLatitude": "51.415672", + "@LaCentreLongitude": "-0.191814", + "@LaCentreLatitudeWGS84": "6695153.285882", + "@LaCentreLongitudeWGS84": "-21352.636807", + "Site": [ + { + "@BulletinDate": "2017-08-03 03:00:00", + "@SiteCode": "ME2", + "@SiteName": "Merton - Merton Road", + "@SiteType": "Roadside", + "@Latitude": "51.4161384794862", + "@Longitude": "-0.192230805042824", + "@LatitudeWGS84": "6695236.54926", + "@LongitudeWGS84": "-21399.0353321", + "Species": { + "@SpeciesCode": "PM10", + "@SpeciesDescription": "PM10 Particulate", + "@AirQualityIndex": "2", + "@AirQualityBand": "Low", + "@IndexSource": "Trigger" + } + }, + { + "@BulletinDate": "2017-08-03 03:00:00", + "@SiteCode": "ME9", + "@SiteName": "Merton - Morden Civic Centre 2", + "@SiteType": "Roadside", + "@Latitude": "51.40162", + "@Longitude": "-0.19589212", + "@LatitudeWGS84": "6692543.79001", + "@LongitudeWGS84": "-21810.7165116", + "Species": { + "@SpeciesCode": "NO2", + "@SpeciesDescription": "Nitrogen Dioxide", + "@AirQualityIndex": "1", + "@AirQualityBand": "Low", + "@IndexSource": "Measurement" + } + } + ] + } + ] + } +}