#!/usr/bin/python
# -*- coding: utf-8 -*-
"""Simple service for SL (Storstockholms Lokaltrafik)."""
import datetime
import json
import logging
from datetime import timedelta

import homeassistant.helpers.config_validation as cv
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (ATTR_FRIENDLY_NAME, CONF_SCAN_INTERVAL,
                                 CONF_SENSOR_TYPE, CONF_SENSORS, STATE_OFF,
                                 STATE_ON)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import (async_track_point_in_utc_time,
                                         async_track_utc_time_change,
                                         track_time_interval)
from homeassistant.util import Throttle
from homeassistant.util.dt import now

from hasl import (haslapi, fpapi, tl2api, ri4api, si2api,
                  HASL_Error, HASL_API_Error, HASL_HTTP_Error)

__version__ = '2.2.0'
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'hasl'

# Keys used in the configuration.
CONF_RI4_KEY = 'ri4key'
CONF_SI2_KEY = 'si2key'
CONF_TL2_KEY = 'tl2key'
CONF_SITEID = 'siteid'
CONF_LINES = 'lines'
CONF_DIRECTION = 'direction'
CONF_ENABLED_SENSOR = 'sensor'
CONF_TIMEWINDOW = 'timewindow'
CONF_SENSORPROPERTY = 'property'
CONF_TRAIN_TYPE = 'train_type'
CONF_TRAFFIC_CLASS = 'traffic_class'
CONF_VERSION = 'version_sensor'
CONF_USE_MINIMIZATION = 'api_minimization'

LIST_SENSOR_TYPES = ['departures', 'status', 'trainlocation', 'comb', 'tl2']
LIST_SENSOR_PROPERTIES = ['min', 'time', 'deviations', 'refresh', 'updated']
LIST_TRAIN_TYPES = ['PT', 'RB', 'TVB', 'SB', 'LB', 'SpvC', 'TB1', 'TB2', 'TB3']

# Default values for configuration.
DEFAULT_INTERVAL = timedelta(minutes=10)
DEFAULT_TIMEWINDOW = 30
DEFAULT_DIRECTION = 0
DEFAULT_SENSORPROPERTY = 'min'
DEFAULT_TRAIN_TYPE = 'PT'
DEFAULT_TRAFFIC_CLASS = ['metro', 'train', 'local', 'tram', 'bus', 'fer']

DEFAULT_SENSORTYPE = 'departures'
DEFAULT_CACHE_FILE = '.storage/haslcache.json'

# Defining the configuration schema.
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
    # API Keys
    vol.Optional(CONF_RI4_KEY): cv.string,
    vol.Optional(CONF_SI2_KEY): cv.string,
    vol.Optional(CONF_TL2_KEY): cv.string,
    vol.Optional(CONF_VERSION, default=False): cv.boolean,
    vol.Optional(CONF_USE_MINIMIZATION, default=True): cv.boolean,

    vol.Required(CONF_SENSORS, default=[]):
        vol.All(cv.ensure_list, [vol.All({

            vol.Required(ATTR_FRIENDLY_NAME): cv.string,

            vol.Required(CONF_SENSOR_TYPE, default=DEFAULT_SENSORTYPE):
                vol.In(LIST_SENSOR_TYPES),

            vol.Optional(CONF_ENABLED_SENSOR): cv.string,

            vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_INTERVAL):
                vol.Any(cv.time_period, cv.positive_timedelta),

            vol.Optional(CONF_SITEID): cv.string,

            vol.Optional(CONF_LINES, default=[]):
                vol.All(cv.ensure_list, [cv.string]),

            vol.Optional(CONF_DIRECTION, default=DEFAULT_DIRECTION):
                vol.All(vol.Coerce(int), vol.Range(min=0, max=2)),

            vol.Optional(CONF_TIMEWINDOW, default=DEFAULT_TIMEWINDOW):
                vol.All(vol.Coerce(int), vol.Range(min=0, max=60)),

            vol.Optional(CONF_SENSORPROPERTY, default=DEFAULT_SENSORPROPERTY):
                vol.In(LIST_SENSOR_PROPERTIES),

            vol.Optional(CONF_TRAFFIC_CLASS, default=DEFAULT_TRAFFIC_CLASS):
                vol.All(cv.ensure_list, [vol.In(DEFAULT_TRAFFIC_CLASS)]),

            vol.Optional(CONF_TRAIN_TYPE, default=DEFAULT_TRAIN_TYPE):
                vol.In(LIST_TRAIN_TYPES)
            })]),
}, extra=vol.ALLOW_EXTRA)


def setup_platform(hass, config, add_devices, discovery_info=None):
    """Setup the sensors."""

    if not hass.data.get(DOMAIN):
        hass.data[DOMAIN] = {}

    sensors = []

    if config[CONF_VERSION]:
        sensors.append(SLVersionSensor(hass))
        _LOGGER.info("Created version sensor for HASL")

    for sensorconf in config[CONF_SENSORS]:

        if sensorconf[CONF_SENSOR_TYPE] == 'departures' or \
           sensorconf[CONF_SENSOR_TYPE] == 'comb':

            sitekey = sensorconf.get(CONF_SITEID)
            si2key = config.get(CONF_SI2_KEY)
            ri4key = config.get(CONF_RI4_KEY)
            if sitekey and ri4key:
                sensorname = sensorconf[ATTR_FRIENDLY_NAME]
                sensors.append(SLDeparturesSensor(
                    hass,
                    si2key,
                    ri4key,
                    sitekey,
                    sensorconf.get(CONF_LINES),
                    sensorname,
                    sensorconf.get(CONF_ENABLED_SENSOR),
                    sensorconf.get(CONF_SCAN_INTERVAL),
                    sensorconf.get(CONF_DIRECTION),
                    sensorconf.get(CONF_TIMEWINDOW),
                    sensorconf.get(CONF_SENSORPROPERTY),
                    config.get(CONF_USE_MINIMIZATION)
                    ))

                _LOGGER.info("Created departures sensor %s...", sensorname)
            else:
                _LOGGER.error("Sensor %s is missing site, si2key or ri4key",
                              sensorconf[ATTR_FRIENDLY_NAME])

        if sensorconf[CONF_SENSOR_TYPE] == 'status' or \
           sensorconf[CONF_SENSOR_TYPE] == 'tl2':

            tl2key = config.get(CONF_TL2_KEY)
            if tl2key:
                sensorname = sensorconf[ATTR_FRIENDLY_NAME]
                sensors.append(SLStatusSensor(
                    hass,
                    tl2key,
                    sensorname,
                    sensorconf.get(CONF_ENABLED_SENSOR),
                    sensorconf.get(CONF_SCAN_INTERVAL),
                    sensorconf.get(CONF_TRAFFIC_CLASS),
                    config.get(CONF_USE_MINIMIZATION)
                    ))

                _LOGGER.info("Created status sensor %s...", sensorname)
            else:
                _LOGGER.error("Sensor %s is missing tl2key attribute",
                              sensorconf[ATTR_FRIENDLY_NAME])

        if sensorconf[CONF_SENSOR_TYPE] == 'trainlocation':
            train_type = sensorconf.get(CONF_TRAIN_TYPE)
            if train_type:
                sensorname = sensorconf[ATTR_FRIENDLY_NAME]
                sensors.append(SLTrainLocationSensor(
                    hass,
                    sensorname,
                    train_type,
                    sensorconf.get(CONF_SCAN_INTERVAL),
                    sensorconf.get(CONF_ENABLED_SENSOR),
                    ))

                _LOGGER.info("Created train sensor %s...", sensorname)
            else:
                _LOGGER.error("Sensor %s is missing train_type attribute",
                              sensorconf[ATTR_FRIENDLY_NAME])

    add_devices(sensors)


class SLTrainLocationSensor(Entity):
    """Trafic Situation Sensor."""
    def __init__(self, hass, friendly_name, train_type,
                 interval, enabled_sensor):

        self._hass = hass
        self._fpapi = fpapi()
        self._name = friendly_name
        self._interval = interval
        self._enabled_sensor = enabled_sensor
        self._train_type = train_type
        self._data = {}

        self.update = Throttle(interval)(self._update)

    @property
    def name(self):
        """Return the name of the sensor."""
        return self._name

    @property
    def icon(self):
        """ Return the icon for the frontend."""
        return None

    @property
    def device_state_attributes(self):
        """ Return the sensor attributes."""
        return {'type': self._train_type, 'data': json.dumps(self._data)}

    @property
    def state(self):
        """ Return the state of the sensor."""
        return self._train_type

    def _update(self):

        if self._enabled_sensor is not None:
            sensor_state = self._hass.states.get(self._enabled_sensor)

        if self._enabled_sensor is None or sensor_state.state is STATE_ON:

            try:
                apidata = self._fpapi.request(self._train_type)

            except HASL_Error as e:
                _LOGGER.error("A communication error occured while "
                              "updating train location sensor: %s", e.details)
                return

            except Exception as e:
                _LOGGER.error("A error occured while"
                              "updating train location sensor: %s", e)
                return

        self._data = apidata

        _LOGGER.info("Update completed %s...", self._name)


class SLVersionSensor(Entity):
    """HASL Version Sensor."""
    def __init__(self, hass):

        self._hass = hass
        self._haslapi = haslapi()
        self._name = 'HASL Version'
        self._version = __version__
        self._py_version = self._haslapi.version()

    @property
    def name(self):
        """Return the name of the sensor."""
        return self._name

    @property
    def icon(self):
        """ Return the icon for the frontend."""
        return None

    @property
    def device_state_attributes(self):
        """ Return the sensor attributes."""
        return {'hasl': self._version, 'pyHasl': self._py_version}

    @property
    def state(self):
        """ Return the state of the sensor."""
        return self._version + "/" + self._py_version


class SLStatusSensor(Entity):
    """Trafic Situation Sensor."""
    def __init__(self, hass, tl2key, friendly_name,
                 enabled_sensor, interval, type,
                 minimization):

        self._tl2api = tl2api(tl2key)
        self._datakey = 'tl2_' + tl2key
        self._interval = interval
        self._hass = hass
        self._name = friendly_name
        self._enabled_sensor = enabled_sensor
        self._type = type
        self._sensordata = []
        self._lastupdate = '-'
        self._cachefile = hass.config.path(DEFAULT_CACHE_FILE)
        self._minimization = minimization

        if not hass.data[DOMAIN].get(self._datakey):
            hass.data[DOMAIN][self._datakey] = ''

        self.update = Throttle(interval)(self._update)

    @property
    def name(self):
        """Return the name of the sensor."""
        return self._name

    @property
    def icon(self):
        """ Return the icon for the frontend."""
        return 'mdi:train-car'

    @property
    def device_state_attributes(self):
        """ Return the sensor attributes."""
        return self._sensordata

    @property
    def state(self):
        """ Return the state of the sensor."""
        return self._lastupdate

    def getCache(self, key):
        try:
            jsonFile = open(self._cachefile, 'r')
            data = json.load(jsonFile)
            jsonFile.close()

            return data.get(key)
        except:
            return {}

    def putCache(self, key, value):
        try:
            jsonFile = open(self._cachefile, 'r')
            data = json.load(jsonFile)
            jsonFile.close()

            data[key] = value
        except:
            data = {'' + key + '': value}

        jsonFile = open(self._cachefile, 'w')
        jsonFile.write(json.dumps(data))
        jsonFile.close()

    def _update(self):

        if self._enabled_sensor is not None:
            sensor_state = self._hass.states.get(self._enabled_sensor)

        if self._enabled_sensor is None or sensor_state.state is STATE_ON:

            _LOGGER.info("Starting to update TL2 for %s...",
                         self._name)

            # Object used to create our object.
            newdata = {}

            # Use some nice translations for the statuses etc.
            statuses = {
                'EventGood': 'Good',
                'EventMinor': 'Minor',
                'EventMajor': 'Closed',
                'EventPlanned': 'Planned',
                }

            # Icon table used for HomeAssistant.
            statusIcons = {
                'EventGood': 'mdi:check',
                'EventMinor': 'mdi:clock-alert-outline',
                'EventMajor': 'mdi:close',
                'EventPlanned': 'mdi:triangle-outline'
                }

            trafficTypeIcons = {
                'ferry': 'mdi:ferry',
                'bus': 'mdi:bus',
                'tram': 'mdi:tram',
                'train': 'mdi:train',
                'local': 'mdi:train-variant',
                'metro': 'mdi:subway-variant'
                }

            # If the same API have already made the request in within
            # the specified interval then use that data instead of
            # requesting it again and spare some innocent credits from dying.
            cacheage = self._hass.data[DOMAIN][self._datakey]
            if not cacheage or now(self._hass.config.time_zone) \
                    - self._interval > cacheage or not self._minimization:

                try:
                    apidata = self._tl2api.request()
                    apidata = apidata['ResponseData']['TrafficTypes']

                    self.putCache(self._datakey, apidata)
                    self._hass.data[DOMAIN][self._datakey] = \
                        now(self._hass.config.time_zone)

                    _LOGGER.info("Updated cache for %s...", self._name)

                except HASL_Error as e:
                    _LOGGER.error("A communication error occured while "
                                  "updating TL2 sensor: %s", e.details)
                    return

                except Exception as e:
                    _LOGGER.error("A error occured while "
                                  "updating TL4 API: %s", e)
                    return

            else:
                apidata = self.getCache(self._datakey)
                _LOGGER.info("Reusing data from cache for %s...",
                             self._name)

            # Return only the relevant portion of the results.
            for response in apidata:
                type = response['Type']
                if self._type is None or type in self._type:
                    statustype = ('ferry' if type == 'fer' else type)
                    newdata[statustype + '_status'] = \
                        statuses.get(response['StatusIcon'])
                    newdata[statustype + '_status_icon'] = \
                        statusIcons.get(response['StatusIcon'])
                    newdata[statustype + '_icon'] = \
                        trafficTypeIcons.get(statustype)

                    for event in response['Events']:
                        event['Status'] = statuses.get(event['StatusIcon'])
                        event['StatusIcon'] = \
                            statusIcons.get(event['StatusIcon'])

                    newdata[statustype + '_events'] = response['Events']

            # Attribution and update sensor data.
            newdata['attribution'] = "Stockholms Lokaltrafik"
            newdata['last_updated'] = \
                self._hass.data[DOMAIN][self._datakey].strftime('%Y-%m-%d' +
                                                                '%H:%M:%S')
            self._sensordata = newdata
            self._lastupdate = newdata['last_updated']

            _LOGGER.info("TL2 update completed for %s...", self._name)


class SLDeparturesSensor(Entity):
    """Departure board for one SL site."""

    def __init__(self, hass, si2key, ri4key, siteid,
                 lines, friendly_name, enabled_sensor,
                 interval, direction, timewindow, sensorproperty,
                 minimization):
        """Initialize"""

        # The table of resulttypes and the corresponding units of measure.
        unit_table = {
            'min': 'min',
            'time': '',
            'deviations': '',
            'refresh': '',
            'update': '',
            }

        if si2key:
            self._si2key = si2key
            self._si2api = si2api(si2key, siteid, '')
            self._si2datakey = 'si2_' + si2key + '_' + siteid

        self._ri4key = ri4key
        self._ri4api = ri4api(ri4key, siteid, 60)
        self._ri4datakey = 'ri2_' + ri4key + '_' + siteid
        self._hass = hass
        self._name = friendly_name
        self._lines = lines
        self._siteid = siteid
        self._enabled_sensor = enabled_sensor
        self._sensorproperty = sensorproperty
        self._departure_table = []
        self._deviations_table = []
        self._direction = direction
        self._timewindow = timewindow
        self._nextdeparture_minutes = '0'
        self._nextdeparture_expected = '-'
        self._lastupdate = '-'
        self._interval = interval
        self._unit_of_measure = unit_table.get(self._sensorproperty, 'min')
        self._cachefile = hass.config.path(DEFAULT_CACHE_FILE)
        self._minimization = minimization

        if not hass.data[DOMAIN].get(self._ri4datakey):
            hass.data[DOMAIN][self._ri4datakey] = ''

        if self._si2key:
            if not hass.data[DOMAIN].get(self._si2datakey):
                hass.data[DOMAIN][self._si2datakey] = ''

        # Setup updating of the sensor.
        self.update = Throttle(interval)(self._update)

    @property
    def name(self):
        """Return the name of the sensor."""
        return self._name

    @property
    def icon(self):
        """ Return the icon for the frontend."""

        if self._deviations_table:
            return 'mdi:bus-alert'

        return 'mdi:bus'

    @property
    def state(self):
        """ Return number of minutes to the next departure """

        # If the sensor should return minutes to next departure.
        if self._sensorproperty is 'min':
            if not self._departure_table:
                return '-'
            return self._departure_table[0]['time']

        # If the sensor should return the time at which next departure occurs.
        if self._sensorproperty is 'time':
            if not self._departure_table:
                return '-'
            expected = self._departure_table[0]['expected'] or '-'
            if expected is not '-':
                expected = \
                    datetime.datetime.strptime(self._nextdeparture_expected,
                                               '%Y-%m-%dT%H:%M:%S')
                expected = expected.strftime('%H:%M:%S')
            return expected

        # If the sensor should return the number of deviations.
        if self._sensorproperty is 'deviations':
            return len(self._deviations_table)

        # If the sensor should return if it is updating or not.
        if self._sensorproperty is 'refresh':
            if self._enabled_sensor is None or sensor_state.state is STATE_ON:
                return STATE_ON
            return STATE_OFF

        if self._sensorproperty is 'updated':
            if self._lastupdate is '-':
                return '-'
            return refresh.strftime('%Y-%m-%d %H:%M:%S')

        # Failsafe
        return '-'

    @property
    def device_state_attributes(self):
        """ Return the sensor attributes ."""

        # Initialize the state attributes.
        val = {}

        # Format the next exptected time.
        if self._departure_table:
            expected_time = self._departure_table[0]['expected'] or '-'
            expected_minutes = self._departure_table[0]['time'] or '-'
            if expected_time is not '-':
                expected_time = \
                    datetime.datetime.strptime(expected_time,
                                               '%Y-%m-%dT%H:%M:%S')
                expected_time = expected_time.strftime('%H:%M:%S')
        else:
            expected_time = '-'
            expected_minutes = '-'

        # Format the last refresh time.
        refresh = self._lastupdate
        if self._lastupdate is not '-':
            refresh = refresh.strftime('%Y-%m-%d %H:%M:%S')

        # Setup the unit of measure.
        if self._unit_of_measure is not '':
            val['unit_of_measurement'] = self._unit_of_measure

        # Check if sensor is currently updating or not.
        if self._enabled_sensor is not None:
            sensor_state = self._hass.states.get(self._enabled_sensor)

        if self._enabled_sensor is None or sensor_state.state is STATE_ON:
            val['refresh_enabled'] = STATE_ON
        else:
            val['refresh_enabled'] = STATE_OFF

        # Set values of the sensor.
        val['attribution'] = 'Stockholms Lokaltrafik'
        val['departures'] = self._departure_table
        val['deviations'] = self._deviations_table
        val['last_refresh'] = refresh
        val['next_departure_minutes'] = expected_minutes
        val['next_departure_time'] = expected_time
        val['deviation_count'] = len(self._deviations_table)

        return val

    def parseDepartureTime(self, t):
        """ weird time formats from the API,
        do some quick and dirty conversions. """

        try:
            if t == 'Nu':
                return 0
            s = t.split()
            if len(s) > 1 and s[1] == 'min':
                return int(s[0])
            s = t.split(':')
            if len(s) > 1:
                rightnow = now(self._hass.config.time_zone)
                min = int(s[0]) * 60 + int(s[1]) - (rightnow.hour * 60 +
                                                    rightnow.minute)
                if min < 0:
                    min = min + 1440
                return min
        except Exception:
            _LOGGER.warning("Failed to parse departure time (%s) ", t)
        return 0

    def getCache(self, key):
        try:
            jsonFile = open(self._cachefile, 'r')
            data = json.load(jsonFile)
            jsonFile.close()

            return data.get(key)
        except:
            return {}

    def putCache(self, key, value):
        try:
            jsonFile = open(self._cachefile, 'r')
            data = json.load(jsonFile)
            jsonFile.close()

            data[key] = value
        except:
            data = {'' + key + '': value}

        jsonFile = open(self._cachefile, 'w')
        jsonFile.write(json.dumps(data))
        jsonFile.close()

    def _update(self):
        """Get the departure board."""

        # If using external sensor, get its value.
        if self._enabled_sensor is not None:
            sensor_state = self._hass.states.get(self._enabled_sensor)

        # If we dont have external sensor or it is ON then proceed.
        if self._enabled_sensor is None or sensor_state.state \
                is STATE_ON:

            self._update_ri4()

            if self._si2key:
                self._update_si2()

            self._lastupdate = now(self._hass.config.time_zone)

    def _update_ri4(self):
        errorOccured = False
        _LOGGER.info("Starting to update RI4 for %s...", self._name)

        cacheage = self._hass.data[DOMAIN][self._ri4datakey]
        if not cacheage or now(self._hass.config.time_zone) \
                - self._interval > cacheage or not self._minimization:

            try:
                departuredata = self._ri4api.request()
                departuredata = departuredata['ResponseData']

                self.putCache(self._ri4datakey, departuredata)
                self._hass.data[DOMAIN][self._ri4datakey] = \
                    now(self._hass.config.time_zone)

                _LOGGER.info("Updated cache for %s...", self._name)

            except HASL_Error as e:
                _LOGGER.error("A communication error occured while "
                              "updating SI2 sensor: %s", e.details)
                errorOccured = True

            except Exception as e:
                _LOGGER.error("A communication error occured while "
                              "updating RI4 API: %s", e)
                errorOccured = True

        else:
            try:
                departuredata = self.getCache(self._ri4datakey)

                _LOGGER.info("Reusing data from cache for %s...",
                             self._name)
            except Exception as e:
                _LOGGER.error("A error occured while retreiving "
                              "cached RI4 sensor data: %s", e)
                errorOccured = True

        if not errorOccured:
            departures = []

            iconswitcher = {
                'Buses': 'mdi:bus',
                'Trams': 'mdi:tram',
                'Ships': 'mdi:ferry',
                'Metros': 'mdi:subway-variant',
                'Trains': 'mdi:train',
                }

            for (i, traffictype) in enumerate(['Metros', 'Buses', 'Trains',
                                               'Trams', 'Ships']):

                for (idx, value) in enumerate(departuredata[traffictype]):
                    direction = value['JourneyDirection'] or 0
                    displaytime = value['DisplayTime'] or ''
                    destination = value['Destination'] or ''
                    linenumber = value['LineNumber'] or ''
                    expected = value['ExpectedDateTime'] or ''
                    groupofline = value['GroupOfLine'] or ''
                    icon = iconswitcher.get(traffictype, 'mdi:train-car')
                    if int(self._direction) == 0 or int(direction) \
                            == int(self._direction):
                        if self._lines == [] or linenumber \
                                in self._lines:
                            diff = self.parseDepartureTime(displaytime)
                            if diff < self._timewindow:
                                departures.append({
                                    'line': linenumber,
                                    'direction': direction,
                                    'departure': displaytime,
                                    'destination': destination,
                                    'time': diff,
                                    'expected': expected,
                                    'type': traffictype,
                                    'groupofline': groupofline,
                                    'icon': icon,
                                    })

            self._departure_table = sorted(departures,
                                           key=lambda k: k['time'])

            _LOGGER.info("RI4 update completed for %s...", self._name)

    def _update_si2(self):
        errorOccured = False
        _LOGGER.info("Starting to update SI2 for %s...", self._name)

        cacheage = self._hass.data[DOMAIN][self._si2datakey]
        if not cacheage or now(self._hass.config.time_zone) \
                - self._interval > cacheage or not self._minimization:

            try:
                deviationdata = self._si2api.request()
                deviationdata = deviationdata['ResponseData']

                self.putCache(self._si2datakey, deviationdata)
                self._hass.data[DOMAIN][self._si2datakey] = \
                    now(self._hass.config.time_zone)

                _LOGGER.info('Updated cache for %s...', self._name)

            except HASL_Error as e:
                _LOGGER.error("A communication error occured while "
                              "updating SI2 sensor: %s", e.details)
                errorOccured = True

            except Exception as e:
                _LOGGER.error("A error occured while "
                              "updating SI2 sensor: %s", e)
                errorOccured = True

        else:
            try:
                deviationdata = self.getCache(self._si2datakey)

                _LOGGER.info("Reusing data from cache for %s...",
                             self._name)

            except Exception as e:
                _LOGGER.error("A error occured while retreiving "
                              "cached SI2 sensor: %s", e.details)
                errorOccured = True

        if not errorOccured:
            deviations = []
            for (idx, value) in enumerate(deviationdata):
                deviations.append({
                    'updated': value['Updated'],
                    'title': value['Header'],
                    'fromDate': value['FromDateTime'],
                    'toDate': value['UpToDateTime'],
                    'details': value['Details'],
                    'sortOrder': value['SortOrder'],
                    })

            self._deviations_table = \
                sorted(deviations, key=lambda k: k['sortOrder'])

            _LOGGER.info("SI2 update completed for %s...", self._name)

[((26, 10, 26, 37), 'logging.getLogger', 'logging.getLogger', ({(26, 28, 26, 36): '__name__'}, {}), '(__name__)', False, 'import logging\n'), ((49, 19, 49, 40), 'datetime.timedelta', 'timedelta', (), '', False, 'from datetime import timedelta\n'), ((62, 4, 62, 30), 'voluptuous.Optional', 'vol.Optional', ({(62, 17, 62, 29): 'CONF_RI4_KEY'}, {}), '(CONF_RI4_KEY)', True, 'import voluptuous as vol\n'), ((63, 4, 63, 30), 'voluptuous.Optional', 'vol.Optional', ({(63, 17, 63, 29): 'CONF_SI2_KEY'}, {}), '(CONF_SI2_KEY)', True, 'import voluptuous as vol\n'), ((64, 4, 64, 30), 'voluptuous.Optional', 'vol.Optional', ({(64, 17, 64, 29): 'CONF_TL2_KEY'}, {}), '(CONF_TL2_KEY)', True, 'import voluptuous as vol\n'), ((65, 4, 65, 45), 'voluptuous.Optional', 'vol.Optional', (), '', True, 'import voluptuous as vol\n'), ((66, 4, 66, 53), 'voluptuous.Optional', 'vol.Optional', (), '', True, 'import voluptuous as vol\n'), ((68, 4, 68, 42), 'voluptuous.Required', 'vol.Required', (), '', True, 'import voluptuous as vol\n'), ((193, 22, 193, 29), 'hasl.fpapi', 'fpapi', ({}, {}), '()', False, 'from hasl import haslapi, fpapi, tl2api, ri4api, si2api, HASL_Error, HASL_API_Error, HASL_HTTP_Error\n'), ((252, 24, 252, 33), 'hasl.haslapi', 'haslapi', ({}, {}), '()', False, 'from hasl import haslapi, fpapi, tl2api, ri4api, si2api, HASL_Error, HASL_API_Error, HASL_HTTP_Error\n'), ((284, 23, 284, 37), 'hasl.tl2api', 'tl2api', ({(284, 30, 284, 36): 'tl2key'}, {}), '(tl2key)', False, 'from hasl import haslapi, fpapi, tl2api, ri4api, si2api, HASL_Error, HASL_API_Error, HASL_HTTP_Error\n'), ((469, 23, 469, 49), 'hasl.ri4api', 'ri4api', ({(469, 30, 469, 36): 'ri4key', (469, 38, 469, 44): 'siteid', (469, 46, 469, 48): '60'}, {}), '(ri4key, siteid, 60)', False, 'from hasl import haslapi, fpapi, tl2api, ri4api, si2api, HASL_Error, HASL_API_Error, HASL_HTTP_Error\n'), ((200, 22, 200, 40), 'homeassistant.util.Throttle', 'Throttle', ({(200, 31, 200, 39): 'interval'}, {}), '(interval)', False, 'from homeassistant.util import Throttle\n'), ((215, 50, 215, 72), 'json.dumps', 'json.dumps', ({(215, 61, 215, 71): 'self._data'}, {}), '(self._data)', False, 'import json\n'), ((299, 22, 299, 40), 'homeassistant.util.Throttle', 'Throttle', ({(299, 31, 299, 39): 'interval'}, {}), '(interval)', False, 'from homeassistant.util import Throttle\n'), ((324, 19, 324, 38), 'json.load', 'json.load', ({(324, 29, 324, 37): 'jsonFile'}, {}), '(jsonFile)', False, 'import json\n'), ((334, 19, 334, 38), 'json.load', 'json.load', ({(334, 29, 334, 37): 'jsonFile'}, {}), '(jsonFile)', False, 'import json\n'), ((342, 23, 342, 39), 'json.dumps', 'json.dumps', ({(342, 34, 342, 38): 'data'}, {}), '(data)', False, 'import json\n'), ((465, 27, 465, 53), 'hasl.si2api', 'si2api', ({(465, 34, 465, 40): 'si2key', (465, 42, 465, 48): 'siteid', (465, 50, 465, 52): '""""""'}, {}), "(si2key, siteid, '')", False, 'from hasl import haslapi, fpapi, tl2api, ri4api, si2api, HASL_Error, HASL_API_Error, HASL_HTTP_Error\n'), ((497, 22, 497, 40), 'homeassistant.util.Throttle', 'Throttle', ({(497, 31, 497, 39): 'interval'}, {}), '(interval)', False, 'from homeassistant.util import Throttle\n'), ((627, 19, 627, 38), 'json.load', 'json.load', ({(627, 29, 627, 37): 'jsonFile'}, {}), '(jsonFile)', False, 'import json\n'), ((637, 19, 637, 38), 'json.load', 'json.load', ({(637, 29, 637, 37): 'jsonFile'}, {}), '(jsonFile)', False, 'import json\n'), ((645, 23, 645, 39), 'json.dumps', 'json.dumps', ({(645, 34, 645, 38): 'data'}, {}), '(data)', False, 'import json\n'), ((664, 31, 664, 63), 'homeassistant.util.dt.now', 'now', ({(664, 35, 664, 62): 'self._hass.config.time_zone'}, {}), '(self._hass.config.time_zone)', False, 'from homeassistant.util.dt import now\n'), ((530, 20, 531, 67), 'datetime.datetime.strptime', 'datetime.datetime.strptime', ({(530, 47, 530, 75): 'self._nextdeparture_expected', (531, 47, 531, 66): '"""%Y-%m-%dT%H:%M:%S"""'}, {}), "(self._nextdeparture_expected, '%Y-%m-%dT%H:%M:%S')", False, 'import datetime\n'), ((566, 20, 567, 67), 'datetime.datetime.strptime', 'datetime.datetime.strptime', ({(566, 47, 566, 60): 'expected_time', (567, 47, 567, 66): '"""%Y-%m-%dT%H:%M:%S"""'}, {}), "(expected_time, '%Y-%m-%dT%H:%M:%S')", False, 'import datetime\n'), ((614, 27, 614, 59), 'homeassistant.util.dt.now', 'now', ({(614, 31, 614, 58): 'self._hass.config.time_zone'}, {}), '(self._hass.config.time_zone)', False, 'from homeassistant.util.dt import now\n'), ((680, 20, 680, 52), 'homeassistant.util.dt.now', 'now', ({(680, 24, 680, 51): 'self._hass.config.time_zone'}, {}), '(self._hass.config.time_zone)', False, 'from homeassistant.util.dt import now\n'), ((764, 20, 764, 52), 'homeassistant.util.dt.now', 'now', ({(764, 24, 764, 51): 'self._hass.config.time_zone'}, {}), '(self._hass.config.time_zone)', False, 'from homeassistant.util.dt import now\n'), ((396, 24, 396, 56), 'homeassistant.util.dt.now', 'now', ({(396, 28, 396, 55): 'self._hass.config.time_zone'}, {}), '(self._hass.config.time_zone)', False, 'from homeassistant.util.dt import now\n'), ((671, 27, 671, 59), 'homeassistant.util.dt.now', 'now', ({(671, 31, 671, 58): 'self._hass.config.time_zone'}, {}), '(self._hass.config.time_zone)', False, 'from homeassistant.util.dt import now\n'), ((755, 27, 755, 59), 'homeassistant.util.dt.now', 'now', ({(755, 31, 755, 58): 'self._hass.config.time_zone'}, {}), '(self._hass.config.time_zone)', False, 'from homeassistant.util.dt import now\n'), ((71, 12, 71, 44), 'voluptuous.Required', 'vol.Required', ({(71, 25, 71, 43): 'ATTR_FRIENDLY_NAME'}, {}), '(ATTR_FRIENDLY_NAME)', True, 'import voluptuous as vol\n'), ((73, 12, 73, 70), 'voluptuous.Required', 'vol.Required', (), '', True, 'import voluptuous as vol\n'), ((76, 12, 76, 45), 'voluptuous.Optional', 'vol.Optional', ({(76, 25, 76, 44): 'CONF_ENABLED_SENSOR'}, {}), '(CONF_ENABLED_SENSOR)', True, 'import voluptuous as vol\n'), ((78, 12, 78, 70), 'voluptuous.Optional', 'vol.Optional', (), '', True, 'import voluptuous as vol\n'), ((81, 12, 81, 37), 'voluptuous.Optional', 'vol.Optional', ({(81, 25, 81, 36): 'CONF_SITEID'}, {}), '(CONF_SITEID)', True, 'import voluptuous as vol\n'), ((83, 12, 83, 48), 'voluptuous.Optional', 'vol.Optional', (), '', True, 'import voluptuous as vol\n'), ((86, 12, 86, 67), 'voluptuous.Optional', 'vol.Optional', (), '', True, 'import voluptuous as vol\n'), ((89, 12, 89, 69), 'voluptuous.Optional', 'vol.Optional', (), '', True, 'import voluptuous as vol\n'), ((92, 12, 92, 77), 'voluptuous.Optional', 'vol.Optional', (), '', True, 'import voluptuous as vol\n'), ((95, 12, 95, 75), 'voluptuous.Optional', 'vol.Optional', (), '', True, 'import voluptuous as vol\n'), ((98, 12, 98, 69), 'voluptuous.Optional', 'vol.Optional', (), '', True, 'import voluptuous as vol\n'), ((74, 16, 74, 41), 'voluptuous.In', 'vol.In', ({(74, 23, 74, 40): 'LIST_SENSOR_TYPES'}, {}), '(LIST_SENSOR_TYPES)', True, 'import voluptuous as vol\n'), ((79, 16, 79, 62), 'voluptuous.Any', 'vol.Any', ({(79, 24, 79, 38): 'cv.time_period', (79, 40, 79, 61): 'cv.positive_timedelta'}, {}), '(cv.time_period, cv.positive_timedelta)', True, 'import voluptuous as vol\n'), ((84, 16, 84, 52), 'voluptuous.All', 'vol.All', ({(84, 24, 84, 38): 'cv.ensure_list', (84, 40, 84, 51): '[cv.string]'}, {}), '(cv.ensure_list, [cv.string])', True, 'import voluptuous as vol\n'), ((93, 16, 93, 46), 'voluptuous.In', 'vol.In', ({(93, 23, 93, 45): 'LIST_SENSOR_PROPERTIES'}, {}), '(LIST_SENSOR_PROPERTIES)', True, 'import voluptuous as vol\n'), ((99, 16, 99, 40), 'voluptuous.In', 'vol.In', ({(99, 23, 99, 39): 'LIST_TRAIN_TYPES'}, {}), '(LIST_TRAIN_TYPES)', True, 'import voluptuous as vol\n'), ((387, 31, 387, 63), 'homeassistant.util.dt.now', 'now', ({(387, 35, 387, 62): 'self._hass.config.time_zone'}, {}), '(self._hass.config.time_zone)', False, 'from homeassistant.util.dt import now\n'), ((87, 24, 87, 39), 'voluptuous.Coerce', 'vol.Coerce', ({(87, 35, 87, 38): 'int'}, {}), '(int)', True, 'import voluptuous as vol\n'), ((87, 41, 87, 64), 'voluptuous.Range', 'vol.Range', (), '', True, 'import voluptuous as vol\n'), ((90, 24, 90, 39), 'voluptuous.Coerce', 'vol.Coerce', ({(90, 35, 90, 38): 'int'}, {}), '(int)', True, 'import voluptuous as vol\n'), ((90, 41, 90, 65), 'voluptuous.Range', 'vol.Range', (), '', True, 'import voluptuous as vol\n'), ((96, 41, 96, 70), 'voluptuous.In', 'vol.In', ({(96, 48, 96, 69): 'DEFAULT_TRAFFIC_CLASS'}, {}), '(DEFAULT_TRAFFIC_CLASS)', True, 'import voluptuous as vol\n')]