"""Platform for sensor integration."""
import datetime
import json
import logging
import requests

import voluptuous as vol

from homeassistant.const import TEMP_CELSIUS
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA

_LOGGER = logging.getLogger(__name__)

CONF_SERIAL = 'serial'
CONF_USER_ID = 'user_id'

# Clairy data is stored in the cloud once every 10 minutes
SCAN_INTERVAL = datetime.timedelta(minutes=10)

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_SERIAL): cv.string,
                                          vol.Required(CONF_USER_ID): cv.string})


def setup_platform(_hass, config, add_entities, _discovery_info=None):
    """Set up the sensor platform."""
    serial = config.get(CONF_SERIAL)
    user_id = config.get(CONF_USER_ID)

    if serial is None:
        _LOGGER.error("'serial' missing in configuration")
    elif user_id is None:
        _LOGGER.error("'user_id' missing in configuration")
    else:
        add_entities([Clairy(serial, user_id)])


class Clairy(Entity):
    """Representation of a Sensor."""

    def __init__(self, serial, user_id):
        """Initialize the sensor."""
        self.url = 'http://api.clairy.co'
        self.__token = None
        self.__serial = serial
        self.__user_id = user_id
        self._data = None
        self._profile = None
        self._firmware_version = None

    @property
    def name(self):
        """Return the name of the sensor."""
        return 'Clairy Temperature'

    @property
    def state(self):
        """Return the state of the sensor."""
        if self._data is not None:
            return self._data['Temperature']
        return None

    @property
    def unit_of_measurement(self):
        """Return the unit of measurement."""
        return TEMP_CELSIUS

    def update(self):
        """Fetch new state data for the sensor.

        This is the only method that should fetch new data for Home Assistant.
        self._data is updated with: {'Device': None,
                                     'Serial': None,
                                     'Date': '2019-10-12T15:28:00',
                                     'Temperature': 22.3,
                                     'Humidity': 56.1,
                                     'FanIsOn': False,
                                     'Iaq': 1000.0},
        """
        if self._profile is None:
            self._profile = self._get_profile()
        if self._firmware_version is None:  # just retreive once at the start of hass
            self._firmware_version = self._get_firmware_version()
        try:
            self._data = self.get_all_data()[0]
        except IndexError:
            _LOGGER.error('failed to retrieve data', exc_info=True)

    @property
    def device_state_attributes(self):
        """Return other details about the sensor state."""
        if self._data:
            attributes = dict(humidity=self._data['Humidity'],
                              fan_is_on=self._data['FanIsOn'],
                              iaq=self._data['Iaq'],
                              # Clairy is up to date, if no one requires a firmware update
                              up_to_date=not [clairy for clairy in self._profile['clairy']
                                              if not clairy['FirmwareUpdated']])
            if not attributes['up_to_date']:
                attributes['change_log'] = self._firmware_version['Changelog']
            return attributes
        return None

    @property
    def _token(self):
        """Get the access_token to get device info."""
        if self.__token is None:
            try:
                with open('.token') as json_file:
                    self.__token = json.load(json_file)
                    _LOGGER.info('token loaded from file')
            except FileNotFoundError:
                pass
        if self.__token is not None:
            # When using python 3.6 (or older), use the following code instead of fromisoformat()
            # expires = datetime.datetime.strptime(self.__token['expires'], "%Y-%m-%dT%H:%M:%S")
            expires = datetime.datetime.fromisoformat(self.__token['expires'])
            if expires < (datetime.datetime.now() - datetime.timedelta(seconds=10)):
                # token is expired (or will expire within 10 seconds)
                self.__token = None
                _LOGGER.info('token expired')
        if self.__token is None:
            now = datetime.datetime.now()
            token = requests.post(
                f'{self.url}/token',
                data=dict(grant_type="password",
                          username="appClairy",
                          Password="<PASSWORD>")).json()
            expires = now + datetime.timedelta(seconds=token['expires_in'])
            self.__token = dict(access_token=token['access_token'],
                                expires=expires.isoformat().split('.')[0])  # remove microseconds
            with open('.token', 'w') as outfile:
                json.dump(self.__token, outfile)
            _LOGGER.info('new token retrieved')
        return self.__token['access_token']

    @property
    def _headers(self):
        return dict(Authorization=f'Bearer {self._token}')

    def _get_version(self):
        """Retrieve the version of the API.

        @return {'baseurl': 'api.clairy.co',
                 'version': 4}
        """
        return requests.get(f'{self.url}/api/GetBaseUrl/Init?Version=1').json()

    def _get_firmware_version(self):
        """Retrieve the firmware version.

        @return {'File': '/Files/F091601_beta48_0x4E63819F.BIN',
                 'Version': 'F091601_beta48',
                 'CRC': '4E63819F',
                 'Changelog': '- Clairy connection bug fixed'}
        """
        return requests.get(f'{self.url}/api/Device/GetLastFirmware',
                            headers=self._headers).json()['firmware']

    def _get_file(self):
        """Retrieve a new firmware image."""
        return requests.get(f'{self.url}/{self._firmware_version["File"]}')

    def _get_profile(self):
        """Retrieve the user and clairy device profile.

        @return {'user': {'Id': '<self.__user_id>',
                          'FullName': None,
                          'Gender': 0,
                          'BirthDate': '<yyyy-mm-ddT00:00:00>',
                          'Email': '<name>@<provider>'},
                 'clairy': [{'ID': '<self.__serial>',
                             'Label': 'Clairy',
                             'Latitude': 0.0,
                             'Longitude': 0.0,
                             'City': '<city>',
                             'TreeType': 3,
                             'Color': 3,
                             'Location': 3,
                             'IsDeviceOnline': False,
                             'Iaq': 1000.0,
                             'FirmwareUpdated': True,
                             'FirmwareVersion': 'F091601_beta48'}]}
        """
        profile = requests.get(f'{self.url}/api/User/profile?userId={self.__user_id}').json()
        _LOGGER.debug('profile=%s', profile)
        return profile


    def get_instant_data(self):
        """Get instant data.

        @return {'Timers': [],
                 'Device': None,
                 'Serial': '<self.__serial>',
                 'Date': '2019-10-12T13:20:17',
                 'Temperature': 22.7,
                 'Humidity': 56.4,
                 'FanIsOn': False,
                 'Iaq': 1000.0}
        """
        # TODO: what does this do? It seems to retrieve old data...
        return requests.get(f'{self.url}/api/Device/GetInstantData?Serial={self.__serial}',
                            headers=self._headers).json()['data']

    def get_all_data(self, start_date=None):
        """Retrieve all measured data for the specified period until now.

        @return [{'Device': None,
                  'Serial': None,
                  'Date': '2019-12-04T11:22:00',
                  'Temperature': 23.5,
                  'Humidity': 40.6,
                  'FanIsOn': False,
                  'Iaq': 1000.0}]
        """
        if start_date is None:
            start_date = datetime.datetime.utcnow() - datetime.timedelta(hours=1)
        if not isinstance(start_date, str):
            start_date = start_date.isoformat()
        url = f'{self.url}/api/Device/GetAllData?Serial={self.__serial}&StartDate={start_date}'
        _LOGGER.debug('url=%s', url)
        _LOGGER.debug('headers=%s', self._headers)
        return requests.get(url, headers=self._headers).json()

"""Platform for sensor integration."""
import datetime
import json
import logging
import requests

import voluptuous as vol

from homeassistant.const import TEMP_CELSIUS
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA

_LOGGER = logging.getLogger(__name__)

CONF_SERIAL = 'serial'
CONF_USER_ID = 'user_id'

# Clairy data is stored in the cloud once every 10 minutes
SCAN_INTERVAL = datetime.timedelta(minutes=10)

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_SERIAL): cv.string,
                                          vol.Required(CONF_USER_ID): cv.string})


def setup_platform(_hass, config, add_entities, _discovery_info=None):
    """Set up the sensor platform."""
    serial = config.get(CONF_SERIAL)
    user_id = config.get(CONF_USER_ID)

    if serial is None:
        _LOGGER.error("'serial' missing in configuration")
    elif user_id is None:
        _LOGGER.error("'user_id' missing in configuration")
    else:
        add_entities([Clairy(serial, user_id)])


class Clairy(Entity):
    """Representation of a Sensor."""

    def __init__(self, serial, user_id):
        """Initialize the sensor."""
        self.url = 'http://api.clairy.co'
        self.__token = None
        self.__serial = serial
        self.__user_id = user_id
        self._data = None
        self._profile = None
        self._firmware_version = None

    @property
    def name(self):
        """Return the name of the sensor."""
        return 'Clairy Temperature'

    @property
    def state(self):
        """Return the state of the sensor."""
        if self._data is not None:
            return self._data['Temperature']
        return None

    @property
    def unit_of_measurement(self):
        """Return the unit of measurement."""
        return TEMP_CELSIUS

    def update(self):
        """Fetch new state data for the sensor.

        This is the only method that should fetch new data for Home Assistant.
        self._data is updated with: {'Device': None,
                                     'Serial': None,
                                     'Date': '2019-10-12T15:28:00',
                                     'Temperature': 22.3,
                                     'Humidity': 56.1,
                                     'FanIsOn': False,
                                     'Iaq': 1000.0},
        """
        if self._profile is None:
            self._profile = self._get_profile()
        if self._firmware_version is None:  # just retreive once at the start of hass
            self._firmware_version = self._get_firmware_version()
        try:
            self._data = self.get_all_data()[0]
        except IndexError:
            _LOGGER.error('failed to retrieve data', exc_info=True)

    @property
    def device_state_attributes(self):
        """Return other details about the sensor state."""
        if self._data:
            attributes = dict(humidity=self._data['Humidity'],
                              fan_is_on=self._data['FanIsOn'],
                              iaq=self._data['Iaq'],
                              # Clairy is up to date, if no one requires a firmware update
                              up_to_date=not [clairy for clairy in self._profile['clairy']
                                              if not clairy['FirmwareUpdated']])
            if not attributes['up_to_date']:
                attributes['change_log'] = self._firmware_version['Changelog']
            return attributes
        return None

    @property
    def _token(self):
        """Get the access_token to get device info."""
        if self.__token is None:
            try:
                with open('.token') as json_file:
                    self.__token = json.load(json_file)
                    _LOGGER.info('token loaded from file')
            except FileNotFoundError:
                pass
        if self.__token is not None:
            # When using python 3.6 (or older), use the following code instead of fromisoformat()
            # expires = datetime.datetime.strptime(self.__token['expires'], "%Y-%m-%dT%H:%M:%S")
            expires = datetime.datetime.fromisoformat(self.__token['expires'])
            if expires < (datetime.datetime.now() - datetime.timedelta(seconds=10)):
                # token is expired (or will expire within 10 seconds)
                self.__token = None
                _LOGGER.info('token expired')
        if self.__token is None:
            now = datetime.datetime.now()
            token = requests.post(
                f'{self.url}/token',
                data=dict(grant_type="password",
                          username="appClairy",
                          Password="<PASSWORD>")).json()
            expires = now + datetime.timedelta(seconds=token['expires_in'])
            self.__token = dict(access_token=token['access_token'],
                                expires=expires.isoformat().split('.')[0])  # remove microseconds
            with open('.token', 'w') as outfile:
                json.dump(self.__token, outfile)
            _LOGGER.info('new token retrieved')
        return self.__token['access_token']

    @property
    def _headers(self):
        return dict(Authorization=f'Bearer {self._token}')

    def _get_version(self):
        """Retrieve the version of the API.

        @return {'baseurl': 'api.clairy.co',
                 'version': 4}
        """
        return requests.get(f'{self.url}/api/GetBaseUrl/Init?Version=1').json()

    def _get_firmware_version(self):
        """Retrieve the firmware version.

        @return {'File': '/Files/F091601_beta48_0x4E63819F.BIN',
                 'Version': 'F091601_beta48',
                 'CRC': '4E63819F',
                 'Changelog': '- Clairy connection bug fixed'}
        """
        return requests.get(f'{self.url}/api/Device/GetLastFirmware',
                            headers=self._headers).json()['firmware']

    def _get_file(self):
        """Retrieve a new firmware image."""
        return requests.get(f'{self.url}/{self._firmware_version["File"]}')

    def _get_profile(self):
        """Retrieve the user and clairy device profile.

        @return {'user': {'Id': '<self.__user_id>',
                          'FullName': None,
                          'Gender': 0,
                          'BirthDate': '<yyyy-mm-ddT00:00:00>',
                          'Email': '<name>@<provider>'},
                 'clairy': [{'ID': '<self.__serial>',
                             'Label': 'Clairy',
                             'Latitude': 0.0,
                             'Longitude': 0.0,
                             'City': '<city>',
                             'TreeType': 3,
                             'Color': 3,
                             'Location': 3,
                             'IsDeviceOnline': False,
                             'Iaq': 1000.0,
                             'FirmwareUpdated': True,
                             'FirmwareVersion': 'F091601_beta48'}]}
        """
        profile = requests.get(f'{self.url}/api/User/profile?userId={self.__user_id}').json()
        _LOGGER.debug('profile=%s', profile)
        return profile


    def get_instant_data(self):
        """Get instant data.

        @return {'Timers': [],
                 'Device': None,
                 'Serial': '<self.__serial>',
                 'Date': '2019-10-12T13:20:17',
                 'Temperature': 22.7,
                 'Humidity': 56.4,
                 'FanIsOn': False,
                 'Iaq': 1000.0}
        """
        # TODO: what does this do? It seems to retrieve old data...
        return requests.get(f'{self.url}/api/Device/GetInstantData?Serial={self.__serial}',
                            headers=self._headers).json()['data']

    def get_all_data(self, start_date=None):
        """Retrieve all measured data for the specified period until now.

        @return [{'Device': None,
                  'Serial': None,
                  'Date': '2019-12-04T11:22:00',
                  'Temperature': 23.5,
                  'Humidity': 40.6,
                  'FanIsOn': False,
                  'Iaq': 1000.0}]
        """
        if start_date is None:
            start_date = datetime.datetime.utcnow() - datetime.timedelta(hours=1)
        if not isinstance(start_date, str):
            start_date = start_date.isoformat()
        url = f'{self.url}/api/Device/GetAllData?Serial={self.__serial}&StartDate={start_date}'
        _LOGGER.debug('url=%s', url)
        _LOGGER.debug('headers=%s', self._headers)
        return requests.get(url, headers=self._headers).json()

Platform for sensor integration. # Clairy data is stored in the cloud once every 10 minutes Set up the sensor platform. Representation of a Sensor. Initialize the sensor. Return the name of the sensor. Return the state of the sensor. Return the unit of measurement. Fetch new state data for the sensor.          This is the only method that should fetch new data for Home Assistant.         self._data is updated with: {'Device': None,                                      'Serial': None,                                      'Date': '2019-10-12T15:28:00',                                      'Temperature': 22.3,                                      'Humidity': 56.1,                                      'FanIsOn': False,                                      'Iaq': 1000.0}, # just retreive once at the start of hass Return other details about the sensor state. # Clairy is up to date, if no one requires a firmware update Get the access_token to get device info. # When using python 3.6 (or older), use the following code instead of fromisoformat() # expires = datetime.datetime.strptime(self.__token['expires'], "%Y-%m-%dT%H:%M:%S") # token is expired (or will expire within 10 seconds) # remove microseconds Retrieve the version of the API.          @return {'baseurl': 'api.clairy.co',                  'version': 4} Retrieve the firmware version.          @return {'File': '/Files/F091601_beta48_0x4E63819F.BIN',                  'Version': 'F091601_beta48',                  'CRC': '4E63819F',                  'Changelog': '- Clairy connection bug fixed'} Retrieve a new firmware image. Retrieve the user and clairy device profile.          @return {'user': {'Id': '<self.__user_id>',                           'FullName': None,                           'Gender': 0,                           'BirthDate': '<yyyy-mm-ddT00:00:00>',                           'Email': '<name>@<provider>'},                  'clairy': [{'ID': '<self.__serial>',                              'Label': 'Clairy',                              'Latitude': 0.0,                              'Longitude': 0.0,                              'City': '<city>',                              'TreeType': 3,                              'Color': 3,                              'Location': 3,                              'IsDeviceOnline': False,                              'Iaq': 1000.0,                              'FirmwareUpdated': True,                              'FirmwareVersion': 'F091601_beta48'}]} Get instant data.          @return {'Timers': [],                  'Device': None,                  'Serial': '<self.__serial>',                  'Date': '2019-10-12T13:20:17',                  'Temperature': 22.7,                  'Humidity': 56.4,                  'FanIsOn': False,                  'Iaq': 1000.0} # TODO: what does this do? It seems to retrieve old data... Retrieve all measured data for the specified period until now.          @return [{'Device': None,                   'Serial': None,                   'Date': '2019-12-04T11:22:00',                   'Temperature': 23.5,                   'Humidity': 40.6,                   'FanIsOn': False,                   'Iaq': 1000.0}]