"""Provides a sensor to track various status aspects of a UPS."""
import logging
from datetime import timedelta

import voluptuous as vol

from homeassistant.components.sensor import PLATFORM_SCHEMA
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
    CONF_HOST,
    CONF_PORT,
    CONF_NAME,
    CONF_USERNAME,
    CONF_PASSWORD,
    TEMP_CELSIUS,
    CONF_RESOURCES,
    CONF_ALIAS,
    ATTR_STATE,
    STATE_UNKNOWN,
    POWER_WATT,
)
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle

_LOGGER = logging.getLogger(__name__)

DEFAULT_NAME = "NUT UPS"
DEFAULT_HOST = "localhost"
DEFAULT_PORT = 3493

KEY_STATUS = "ups.status"
KEY_STATUS_DISPLAY = "ups.status.display"

MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)

SENSOR_TYPES = {
    "ups.status.display": ["Status", "", "mdi:information-outline"],
    "ups.status": ["Status Data", "", "mdi:information-outline"],
    "ups.alarm": ["Alarms", "", "mdi:alarm"],
    "ups.time": ["Internal Time", "", "mdi:calendar-clock"],
    "ups.date": ["Internal Date", "", "mdi:calendar"],
    "ups.model": ["Model", "", "mdi:information-outline"],
    "ups.mfr": ["Manufacturer", "", "mdi:information-outline"],
    "ups.mfr.date": ["Manufacture Date", "", "mdi:calendar"],
    "ups.serial": ["Serial Number", "", "mdi:information-outline"],
    "ups.vendorid": ["Vendor ID", "", "mdi:information-outline"],
    "ups.productid": ["Product ID", "", "mdi:information-outline"],
    "ups.firmware": ["Firmware Version", "", "mdi:information-outline"],
    "ups.firmware.aux": ["Firmware Version 2", "", "mdi:information-outline"],
    "ups.temperature": ["UPS Temperature", TEMP_CELSIUS, "mdi:thermometer"],
    "ups.load": ["Load", "%", "mdi:gauge"],
    "ups.load.high": ["Overload Setting", "%", "mdi:gauge"],
    "ups.id": ["System identifier", "", "mdi:information-outline"],
    "ups.delay.start": ["Load Restart Delay", "s", "mdi:timer"],
    "ups.delay.reboot": ["UPS Reboot Delay", "s", "mdi:timer"],
    "ups.delay.shutdown": ["UPS Shutdown Delay", "s", "mdi:timer"],
    "ups.timer.start": ["Load Start Timer", "s", "mdi:timer"],
    "ups.timer.reboot": ["Load Reboot Timer", "s", "mdi:timer"],
    "ups.timer.shutdown": ["Load Shutdown Timer", "s", "mdi:timer"],
    "ups.test.interval": ["Self-Test Interval", "s", "mdi:timer"],
    "ups.test.result": ["Self-Test Result", "", "mdi:information-outline"],
    "ups.test.date": ["Self-Test Date", "", "mdi:calendar"],
    "ups.display.language": ["Language", "", "mdi:information-outline"],
    "ups.contacts": ["External Contacts", "", "mdi:information-outline"],
    "ups.efficiency": ["Efficiency", "%", "mdi:gauge"],
    "ups.power": ["Current Apparent Power", "VA", "mdi:flash"],
    "ups.power.nominal": ["Nominal Power", "VA", "mdi:flash"],
    "ups.realpower": ["Current Real Power", POWER_WATT, "mdi:flash"],
    "ups.realpower.nominal": ["Nominal Real Power", POWER_WATT, "mdi:flash"],
    "ups.beeper.status": ["Beeper Status", "", "mdi:information-outline"],
    "ups.type": ["UPS Type", "", "mdi:information-outline"],
    "ups.watchdog.status": ["Watchdog Status", "", "mdi:information-outline"],
    "ups.start.auto": ["Start on AC", "", "mdi:information-outline"],
    "ups.start.battery": ["Start on Battery", "", "mdi:information-outline"],
    "ups.start.reboot": ["Reboot on Battery", "", "mdi:information-outline"],
    "ups.shutdown": ["Shutdown Ability", "", "mdi:information-outline"],
    "battery.charge": ["Battery Charge", "%", "mdi:gauge"],
    "battery.charge.low": ["Low Battery Setpoint", "%", "mdi:gauge"],
    "battery.charge.restart": ["Minimum Battery to Start", "%", "mdi:gauge"],
    "battery.charge.warning": ["Warning Battery Setpoint", "%", "mdi:gauge"],
    "battery.charger.status": ["Charging Status", "", "mdi:information-outline"],
    "battery.voltage": ["Battery Voltage", "V", "mdi:flash"],
    "battery.voltage.nominal": ["Nominal Battery Voltage", "V", "mdi:flash"],
    "battery.voltage.low": ["Low Battery Voltage", "V", "mdi:flash"],
    "battery.voltage.high": ["High Battery Voltage", "V", "mdi:flash"],
    "battery.capacity": ["Battery Capacity", "Ah", "mdi:flash"],
    "battery.current": ["Battery Current", "A", "mdi:flash"],
    "battery.current.total": ["Total Battery Current", "A", "mdi:flash"],
    "battery.temperature": ["Battery Temperature", TEMP_CELSIUS, "mdi:thermometer"],
    "battery.runtime": ["Battery Runtime", "s", "mdi:timer"],
    "battery.runtime.low": ["Low Battery Runtime", "s", "mdi:timer"],
    "battery.runtime.restart": ["Minimum Battery Runtime to Start", "s", "mdi:timer"],
    "battery.alarm.threshold": [
        "Battery Alarm Threshold",
        "",
        "mdi:information-outline",
    ],
    "battery.date": ["Battery Date", "", "mdi:calendar"],
    "battery.mfr.date": ["Battery Manuf. Date", "", "mdi:calendar"],
    "battery.packs": ["Number of Batteries", "", "mdi:information-outline"],
    "battery.packs.bad": ["Number of Bad Batteries", "", "mdi:information-outline"],
    "battery.type": ["Battery Chemistry", "", "mdi:information-outline"],
    "input.sensitivity": ["Input Power Sensitivity", "", "mdi:information-outline"],
    "input.transfer.low": ["Low Voltage Transfer", "V", "mdi:flash"],
    "input.transfer.high": ["High Voltage Transfer", "V", "mdi:flash"],
    "input.transfer.reason": ["Voltage Transfer Reason", "", "mdi:information-outline"],
    "input.voltage": ["Input Voltage", "V", "mdi:flash"],
    "input.voltage.nominal": ["Nominal Input Voltage", "V", "mdi:flash"],
    "input.frequency": ["Input Line Frequency", "hz", "mdi:flash"],
    "input.frequency.nominal": ["Nominal Input Line Frequency", "hz", "mdi:flash"],
    "input.frequency.status": ["Input Frequency Status", "", "mdi:information-outline"],
    "output.current": ["Output Current", "A", "mdi:flash"],
    "output.current.nominal": ["Nominal Output Current", "A", "mdi:flash"],
    "output.voltage": ["Output Voltage", "V", "mdi:flash"],
    "output.voltage.nominal": ["Nominal Output Voltage", "V", "mdi:flash"],
    "output.frequency": ["Output Frequency", "hz", "mdi:flash"],
    "output.frequency.nominal": ["Nominal Output Frequency", "hz", "mdi:flash"],
}

STATE_TYPES = {
    "OL": "Online",
    "OB": "On Battery",
    "LB": "Low Battery",
    "HB": "High Battery",
    "RB": "Battery Needs Replaced",
    "CHRG": "Battery Charging",
    "DISCHRG": "Battery Discharging",
    "BYPASS": "Bypass Active",
    "CAL": "Runtime Calibration",
    "OFF": "Offline",
    "OVER": "Overloaded",
    "TRIM": "Trimming Voltage",
    "BOOST": "Boosting Voltage",
    "FSD": "Forced Shutdown",
    "ALARM": "Alarm",
}

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
    {
        vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
        vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
        vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
        vol.Optional(CONF_ALIAS): cv.string,
        vol.Optional(CONF_USERNAME): cv.string,
        vol.Optional(CONF_PASSWORD): cv.string,
        vol.Required(CONF_RESOURCES): vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
    }
)


def setup_platform(hass, config, add_entities, discovery_info=None):
    """Set up the NUT sensors."""
    name = config.get(CONF_NAME)
    host = config.get(CONF_HOST)
    port = config.get(CONF_PORT)
    alias = config.get(CONF_ALIAS)
    username = config.get(CONF_USERNAME)
    password = config.get(CONF_PASSWORD)

    data = PyNUTData(host, port, alias, username, password)

    if data.status is None:
        _LOGGER.error("NUT Sensor has no data, unable to set up")
        raise PlatformNotReady

    _LOGGER.debug("NUT Sensors Available: %s", data.status)

    entities = []

    for resource in config[CONF_RESOURCES]:
        sensor_type = resource.lower()

        # Display status is a special case that falls back to the status value
        # of the UPS instead.
        if sensor_type in data.status or (
            sensor_type == KEY_STATUS_DISPLAY and KEY_STATUS in data.status
        ):
            entities.append(NUTSensor(name, data, sensor_type))
        else:
            _LOGGER.warning(
                "Sensor type: %s does not appear in the NUT status "
                "output, cannot add",
                sensor_type,
            )

    try:
        data.update(no_throttle=True)
    except data.pynuterror as err:
        _LOGGER.error(
            "Failure while testing NUT status retrieval. " "Cannot continue setup: %s",
            err,
        )
        raise PlatformNotReady

    add_entities(entities, True)


class NUTSensor(Entity):
    """Representation of a sensor entity for NUT status values."""

    def __init__(self, name, data, sensor_type):
        """Initialize the sensor."""
        self._data = data
        self.type = sensor_type
        self._name = "{} {}".format(name, SENSOR_TYPES[sensor_type][0])
        self._unit = SENSOR_TYPES[sensor_type][1]
        self._state = None

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

    @property
    def icon(self):
        """Icon to use in the frontend, if any."""
        return SENSOR_TYPES[self.type][2]

    @property
    def state(self):
        """Return entity state from ups."""
        return self._state

    @property
    def unit_of_measurement(self):
        """Return the unit of measurement of this entity, if any."""
        return self._unit

    @property
    def device_state_attributes(self):
        """Return the sensor attributes."""
        attr = dict()
        attr[ATTR_STATE] = self.display_state()
        return attr

    def display_state(self):
        """Return UPS display state."""
        if self._data.status is None:
            return STATE_TYPES["OFF"]
        try:
            return " ".join(
                STATE_TYPES[state] for state in self._data.status[KEY_STATUS].split()
            )
        except KeyError:
            return STATE_UNKNOWN

    def update(self):
        """Get the latest status and use it to update our sensor state."""
        if self._data.status is None:
            self._state = None
            return

        # In case of the display status sensor, keep a human-readable form
        # as the sensor state.
        if self.type == KEY_STATUS_DISPLAY:
            self._state = self.display_state()
        elif self.type not in self._data.status:
            self._state = None
        else:
            self._state = self._data.status[self.type]


class PyNUTData:
    """Stores the data retrieved from NUT.

    For each entity to use, acts as the single point responsible for fetching
    updates from the server.
    """

    def __init__(self, host, port, alias, username, password):
        """Initialize the data object."""
        from pynut2.nut2 import PyNUTClient, PyNUTError

        self._host = host
        self._port = port
        self._alias = alias
        self._username = username
        self._password = password

        self.pynuterror = PyNUTError
        # Establish client with persistent=False to open/close connection on
        # each update call.  This is more reliable with async.
        self._client = PyNUTClient(
            self._host, self._port, self._username, self._password, 5, False
        )

        self._status = None

    @property
    def status(self):
        """Get latest update if throttle allows. Return status."""
        self.update()
        return self._status

    def _get_alias(self):
        """Get the ups alias from NUT."""
        try:
            return next(iter(self._client.list_ups()))
        except self.pynuterror as err:
            _LOGGER.error("Failure getting NUT ups alias, %s", err)
            return None

    def _get_status(self):
        """Get the ups status from NUT."""
        if self._alias is None:
            self._alias = self._get_alias()

        try:
            return self._client.list_vars(self._alias)
        except (self.pynuterror, ConnectionResetError) as err:
            _LOGGER.debug("Error getting NUT vars for host %s: %s", self._host, err)
            return None

    @Throttle(MIN_TIME_BETWEEN_UPDATES)
    def update(self, **kwargs):
        """Fetch the latest status from NUT."""
        self._status = self._get_status()