"""pyotgw is a library to interface with the OpenTherm Gateway."""

import asyncio
import logging
from datetime import datetime

from pyotgw import vars as v
from pyotgw.connection import ConnectionManager
from pyotgw.status import StatusManager

_LOGGER = logging.getLogger(__name__)


class OpenThermGateway:  # pylint: disable=too-many-public-methods
    """Main OpenThermGateway object abstraction"""

    def __init__(self):
        """Create an OpenThermGateway object."""
        self._transport = None
        self._protocol = None
        self._gpio_task = None
        self._skip_init = False
        self.status = StatusManager()
        self.connection = ConnectionManager(self)

    async def cleanup(self):
        """Clean up tasks."""
        await self.connection.disconnect()
        await self.status.cleanup()
        if self._gpio_task:
            self._gpio_task.cancel()
            await self._gpio_task

    async def connect(
        self,
        port,
        timeout=None,
        skip_init=None,
    ):
        """
        Connect to Opentherm Gateway at @port.
        Initialize the parameters obtained from the PS= and PR=
        commands and returns the status dict with the obtained values
        or False if cancelled.
        If @skip_init is True, the PS= and PR= commands are skipped and only PS=0 is
        sent upon this and future connection attempts.
        If called while connected, reconnect to the gateway.

        This method is a coroutine
        """
        if skip_init is not None:
            self._skip_init = skip_init
        if not await self.connection.connect(port, timeout):
            return False
        self._protocol = self.connection.protocol
        if not self._skip_init:
            await self.get_reports()
            await self.get_status()
        await self._poll_gpio()
        return self.status.status

    async def disconnect(self):
        """Disconnect from the OpenTherm Gateway."""
        await self.cleanup()
        return await self.connection.disconnect()

    def set_connection_options(self, **kwargs):
        """Set connection parameters."""
        return self.connection.set_connection_config(**kwargs)

    async def set_target_temp(
        self, temp, temporary=True, timeout=v.OTGW_DEFAULT_TIMEOUT
    ):
        """
        Set the thermostat setpoint and return the newly accepted
        value.
        kwarg @temporary specifies whether or not the thermostat
            program may override this temperature.

        This method is a coroutine
        """
        cmd = v.OTGW_CMD_TARGET_TEMP if temporary else v.OTGW_CMD_TARGET_TEMP_CONST
        value = f"{temp:2.1f}"
        ret = await self._wait_for_cmd(cmd, value, timeout)
        if ret is None:
            return None
        ret = float(ret)
        if 0 <= ret <= 30:
            if ret == 0:
                status_update = {
                    v.OTGW: {v.OTGW_SETP_OVRD_MODE: v.OTGW_SETP_OVRD_DISABLED},
                    v.THERMOSTAT: {v.DATA_ROOM_SETPOINT_OVRD: None},
                }
            else:
                if temporary:
                    ovrd_mode = v.OTGW_SETP_OVRD_TEMPORARY
                else:
                    ovrd_mode = v.OTGW_SETP_OVRD_PERMANENT
                status_update = {
                    v.OTGW: {v.OTGW_SETP_OVRD_MODE: ovrd_mode},
                    v.THERMOSTAT: {v.DATA_ROOM_SETPOINT_OVRD: ret},
                }
            self.status.submit_full_update(status_update)
            return ret

    async def set_temp_sensor_function(self, func, timeout=v.OTGW_DEFAULT_TIMEOUT):
        """
        Set the temperature sensor function. The following functions are available:
            O: Outside temperature
            R: Return water temperature

        This method is a coroutine
        """
        cmd = v.OTGW_CMD_TEMP_SENSOR
        if func not in "OR":
            return None
        ret = await self._wait_for_cmd(cmd, func, timeout)
        if ret is None:
            return None
        status_otgw = {v.OTGW_TEMP_SENSOR: ret}
        self.status.submit_partial_update(v.OTGW, status_otgw)
        return ret

    async def set_outside_temp(self, temp, timeout=v.OTGW_DEFAULT_TIMEOUT):
        """
        Configure the outside temperature to send to the thermostat.
        Allowed values are between -40.0 and +64.0, although
        thermostats may not display the full range. Specify a value
        above 64 (suggestion: 99) to clear a previously configured
        value.
        Return the accepted value on success, '-' if a previously
        configured value has been cleared or None on failure.

        This method is a coroutine
        """
        cmd = v.OTGW_CMD_OUTSIDE_TEMP
        status_thermostat = {}
        if temp < -40:
            return None
        value = f"{temp:2.1f}"
        ret = await self._wait_for_cmd(cmd, value, timeout)
        if ret is None:
            return
        if ret == "-":
            status_thermostat[v.DATA_OUTSIDE_TEMP] = 0.0
        else:
            ret = float(ret)
            status_thermostat[v.DATA_OUTSIDE_TEMP] = ret
        self.status.submit_partial_update(v.THERMOSTAT, status_thermostat)
        return ret

    async def set_clock(self, date=datetime.now(), timeout=v.OTGW_DEFAULT_TIMEOUT):
        """
        Change the time and day of the week of the thermostat. The
        gateway will send the specified time and day of the week in
        response to the next time and date message from the thermostat.
        @date is a :datetime: object which defaults to now()
        Return the response from the gateway with format HH:MM/DOW,
        where DOW is a single digit: 1=Monday, 7=Sunday.

        This method is a coroutine
        """
        cmd = v.OTGW_CMD_SET_CLOCK
        value = f"{date.strftime('%H:%M')}/{date.isoweekday()}"
        return await self._wait_for_cmd(cmd, value, timeout)

    async def get_reports(self):
        """
        Update the OpenThermGateway object with the information from all
        of the PR commands and return the updated status dict.

        This method is a coroutine
        """
        cmd = v.OTGW_CMD_REPORT
        reports = {}
        # Get version info first
        ret = await self._wait_for_cmd(cmd, v.OTGW_REPORT_ABOUT)
        reports[v.OTGW_REPORT_ABOUT] = ret[2:] if ret else None
        for value in v.OTGW_REPORTS:

            ver = reports.get(v.OTGW_REPORT_ABOUT)
            if ver and int(ver[18]) < 5 and value == "D":
                # Added in v5
                continue
            if value == v.OTGW_REPORT_ABOUT:
                continue

            ret = await self._wait_for_cmd(cmd, value)
            if ret is None:
                reports[value] = None
                continue
            reports[value] = ret[2:]

        status_otgw = {
            v.OTGW_ABOUT: reports.get(v.OTGW_REPORT_ABOUT),
            v.OTGW_BUILD: reports.get(v.OTGW_REPORT_BUILDDATE),
            v.OTGW_CLOCKMHZ: reports.get(v.OTGW_REPORT_CLOCKMHZ),
            v.OTGW_DHW_OVRD: reports.get(v.OTGW_REPORT_DHW_SETTING),
            v.OTGW_MODE: reports.get(v.OTGW_REPORT_GW_MODE),
            v.OTGW_RST_CAUSE: reports.get(v.OTGW_REPORT_RST_CAUSE),
            v.OTGW_SMART_PWR: reports.get(v.OTGW_REPORT_SMART_PWR),
            v.OTGW_TEMP_SENSOR: reports.get(v.OTGW_REPORT_TEMP_SENSOR),
            v.OTGW_THRM_DETECT: reports.get(v.OTGW_REPORT_THERMOSTAT_DETECT),
        }
        status_thermostat = {}
        ovrd_mode = reports.get(v.OTGW_REPORT_SETPOINT_OVRD)
        if ovrd_mode is not None:
            ovrd_mode = str.upper(ovrd_mode[0])
            status_otgw.update({v.OTGW_SETP_OVRD_MODE: ovrd_mode})
        gpio_funcs = reports.get(v.OTGW_REPORT_GPIO_FUNCS)
        if gpio_funcs is not None:
            status_otgw.update(
                {v.OTGW_GPIO_A: int(gpio_funcs[0]), v.OTGW_GPIO_B: int(gpio_funcs[1])}
            )
        led_funcs = reports.get(v.OTGW_REPORT_LED_FUNCS)
        if led_funcs is not None:
            status_otgw.update(
                {
                    v.OTGW_LED_A: led_funcs[0],
                    v.OTGW_LED_B: led_funcs[1],
                    v.OTGW_LED_C: led_funcs[2],
                    v.OTGW_LED_D: led_funcs[3],
                    v.OTGW_LED_E: led_funcs[4],
                    v.OTGW_LED_F: led_funcs[5],
                }
            )
        tweaks = reports.get(v.OTGW_REPORT_TWEAKS)
        if tweaks is not None:
            status_otgw.update(
                {
                    v.OTGW_IGNORE_TRANSITIONS: int(tweaks[0]),
                    v.OTGW_OVRD_HB: int(tweaks[1]),
                }
            )
        sb_temp = reports.get(v.OTGW_REPORT_SETBACK_TEMP)
        if sb_temp is not None:
            status_otgw.update({v.OTGW_SB_TEMP: float(sb_temp)})
        vref = reports.get(v.OTGW_REPORT_VREF)
        if vref is not None:
            status_otgw.update({v.OTGW_VREF: int(vref)})
        if ovrd_mode is not None and ovrd_mode != v.OTGW_SETP_OVRD_DISABLED:
            status_thermostat = {
                v.DATA_ROOM_SETPOINT_OVRD: float(
                    reports[v.OTGW_REPORT_SETPOINT_OVRD][1:]
                )
            }
        self.status.submit_full_update(
            {v.THERMOSTAT: status_thermostat, v.OTGW: status_otgw}
        )
        return self.status.status

    async def get_status(self):
        """
        Update the OpenThermGateway object with the information from the
        PS command and return the updated status dict.

        This method is a coroutine
        """
        cmd = v.OTGW_CMD_SUMMARY
        ret = await self._wait_for_cmd(cmd, 1)
        # Return to 'reporting' mode
        if ret is None:
            return
        asyncio.get_running_loop().create_task(self._wait_for_cmd(cmd, 0))
        fields = ret[1].split(",")
        if len(fields) == 34:
            # OpenTherm Gateway 5.0
            boiler_status, thermostat_status = process_statusfields_v5(fields)
        else:
            boiler_status, thermostat_status = process_statusfields_v4(fields)
        self.status.submit_full_update({
            v.BOILER: boiler_status,
            v.THERMOSTAT: thermostat_status
        })
        return self.status.status

    async def set_hot_water_ovrd(self, state, timeout=v.OTGW_DEFAULT_TIMEOUT):
        """
        Control the domestic hot water enable option. If the boiler has
        been configured to let the room unit control when to keep a
        small amount of water preheated, this command can influence
        that.
        @state should be 0 or 1 to enable the override in off or on
        state, or any other single character to disable the override.
        Return the accepted value, 'A' if the override is disabled
        or None on failure.

        This method is a coroutine
        """
        cmd = v.OTGW_CMD_HOT_WATER
        status_otgw = {}
        ret = await self._wait_for_cmd(cmd, state, timeout)
        if ret is None:
            return None
        if ret == "A":
            status_otgw[v.OTGW_DHW_OVRD] = None
        elif ret in ["0", "1"]:
            ret = int(ret)
            status_otgw[v.OTGW_DHW_OVRD] = ret
        self.status.submit_partial_update(v.OTGW, status_otgw)
        return ret

    async def set_mode(self, mode, timeout=v.OTGW_DEFAULT_TIMEOUT):
        """
        Set the operating mode to either "Gateway" mode (:mode: =
        OTGW_MODE_GATEWAY or 1) or "Monitor" mode (:mode: =
        OTGW_MODE_MONITOR or 0), or use this method to reset the device
        (:mode: = OTGW_MODE_RESET).
        Return the newly activated mode, or the full renewed status
        dict after a reset.

        This method is a coroutine
        """
        cmd = v.OTGW_CMD_MODE
        status_otgw = {}
        ret = await self._wait_for_cmd(cmd, mode, timeout)
        if ret is None:
            return None
        if mode is v.OTGW_MODE_RESET:
            self.status.reset()
            await self.get_reports()
            await self.get_status()
            return self.status.status
        status_otgw[v.OTGW_MODE] = ret
        self.status.submit_partial_update(v.OTGW, status_otgw)
        return ret

    async def set_led_mode(self, led_id, mode, timeout=v.OTGW_DEFAULT_TIMEOUT):
        """
        Configure the functions of the six LEDs (A-F) that can
        optionally be connected to pins RB3/RB4/RB6/RB7 and the GPIO
        pins of the PIC. The following functions are currently
        available:

        R Receiving an Opentherm message from the thermostat or boiler
        X Transmitting an Opentherm message to the thermostat or boiler
        T Transmitting or receiving a message on the master interface
        B Transmitting or receiving a message on the slave interface
        O Remote setpoint override is active
        F Flame is on
        H Central heating is on
        W Hot water is on
        C Comfort mode (Domestic Hot Water Enable) is on
        E Transmission error has been detected
        M Boiler requires maintenance
        P Raised power mode active on thermostat interface.

        Return the new mode for the specified led, or None on failure.

        This method is a coroutine
        """
        if led_id in "ABCDEF" and mode in "RXTBOFHWCEMP":
            cmd = getattr(v, f"OTGW_CMD_LED_{led_id}")
            status_otgw = {}
            ret = await self._wait_for_cmd(cmd, mode, timeout)
            if ret is None:
                return None
            var = getattr(v, f"OTGW_LED_{led_id}")
            status_otgw[var] = ret
            self.status.submit_partial_update(v.OTGW, status_otgw)
            return ret

    async def set_gpio_mode(self, gpio_id, mode, timeout=v.OTGW_DEFAULT_TIMEOUT):
        """
        Configure the functions of the two GPIO pins of the gateway.
        The following functions are available:

        0 No function, default for both ports on a freshly flashed chip
        1 Ground - A permanently low output (0V). Could be used for a
            power LED
        2 Vcc - A permanently high output (5V). Can be used as a
            short-proof power supply for some external circuitry used
            by the other GPIO port
        3 LED E - An additional LED if you want to present more than 4
            LED functions
        4 LED F - An additional LED if you want to present more than 5
            LED functions
        5 Home - Set thermostat to setback temperature when pulled low
        6 Away - Set thermostat to setback temperature when pulled high
        7 DS1820 (GPIO port B only) - Data line for a DS18S20 or
            DS18B20 temperature sensor used to measure the outside
            temperature. A 4k7 resistor should be connected between
            GPIO port B and Vcc

        Return the new mode for the specified gpio, or None on
        failure.

        This method is a coroutine
        """
        if gpio_id in "AB" and mode in range(8):
            if mode == 7 and gpio_id != "B":
                return None
            cmd = getattr(v, f"OTGW_CMD_GPIO_{gpio_id}")
            status_otgw = {}
            ret = await self._wait_for_cmd(cmd, mode, timeout)
            if ret is None:
                return
            ret = int(ret)
            var = getattr(v, f"OTGW_GPIO_{gpio_id}")
            status_otgw[var] = ret
            self.status.submit_partial_update(v.OTGW, status_otgw)
            asyncio.ensure_future(self._poll_gpio())
            return ret

    async def set_setback_temp(self, sb_temp, timeout=v.OTGW_DEFAULT_TIMEOUT):
        """
        Configure the setback temperature to use in combination with
        GPIO functions HOME (5) and AWAY (6).
        Return the new setback temperature, or None on failure.

        This method is a coroutine
        """
        cmd = v.OTGW_CMD_SETBACK
        status_otgw = {}
        ret = await self._wait_for_cmd(cmd, sb_temp, timeout)
        if ret is None:
            return
        ret = float(ret)
        status_otgw[v.OTGW_SB_TEMP] = ret
        self.status.submit_partial_update(v.OTGW, status_otgw)
        return ret

    async def add_alternative(self, alt, timeout=v.OTGW_DEFAULT_TIMEOUT):
        """
        Add the specified Data-ID to the list of alternative commands
        to send to the boiler instead of a Data-ID that is known to be
        unsupported by the boiler. Alternative Data-IDs will always be
        sent to the boiler in a Read-Data request message with the
        data-value set to zero. The table of alternative Data-IDs is
        stored in non-volatile memory so it will persist even if the
        gateway has been powered off. Data-ID values from 1 to 255 are
        allowed.
        Return the ID that was added to the list, or None on failure.

        This method is a coroutine
        """
        cmd = v.OTGW_CMD_ADD_ALT
        alt = int(alt)
        if alt < 1 or alt > 255:
            return None
        ret = await self._wait_for_cmd(cmd, alt, timeout)
        if ret is not None:
            return int(ret)

    async def del_alternative(self, alt, timeout=v.OTGW_DEFAULT_TIMEOUT):
        """
        Remove the specified Data-ID from the list of alternative
        commands. Only one occurrence is deleted. If the Data-ID
        appears multiple times in the list of alternative commands,
        this command must be repeated to delete all occurrences. The
        table of alternative Data-IDs is stored in non-volatile memory
        so it will persist even if the gateway has been powered off.
        Data-ID values from 1 to 255 are allowed.
        Return the ID that was removed from the list, or None on
        failure.

        This method is a coroutine
        """
        cmd = v.OTGW_CMD_DEL_ALT
        alt = int(alt)
        if alt < 1 or alt > 255:
            return None
        ret = await self._wait_for_cmd(cmd, alt, timeout)
        if ret is not None:
            return int(ret)

    async def add_unknown_id(self, unknown_id, timeout=v.OTGW_DEFAULT_TIMEOUT):
        """
        Inform the gateway that the boiler doesn't support the
        specified Data-ID, even if the boiler doesn't indicate that
        by returning an Unknown-DataId response. Using this command
        allows the gateway to send an alternative Data-ID to the boiler
        instead.
        Return the added ID, or None on failure.

        This method is a coroutine
        """
        cmd = v.OTGW_CMD_UNKNOWN_ID
        unknown_id = int(unknown_id)
        if unknown_id < 1 or unknown_id > 255:
            return None
        ret = await self._wait_for_cmd(cmd, unknown_id, timeout)
        if ret is not None:
            return int(ret)

    async def del_unknown_id(self, unknown_id, timeout=v.OTGW_DEFAULT_TIMEOUT):
        """
        Start forwarding the specified Data-ID to the boiler again.
        This command resets the counter used to determine if the
        specified Data-ID is supported by the boiler.
        Return the ID that was marked as supported, or None on failure.

        This method is a coroutine
        """
        cmd = v.OTGW_CMD_KNOWN_ID
        unknown_id = int(unknown_id)
        if unknown_id < 1 or unknown_id > 255:
            return None
        ret = await self._wait_for_cmd(cmd, unknown_id, timeout)
        if ret is not None:
            return int(ret)

    async def set_max_ch_setpoint(self, temperature, timeout=v.OTGW_DEFAULT_TIMEOUT):
        """
        Set the maximum central heating setpoint. This command is only
        available with boilers that support this function.
        Return the newly accepted setpoint, or None on failure.

        This method is a coroutine
        """
        cmd = v.OTGW_CMD_SET_MAX
        status_boiler = {}
        ret = await self._wait_for_cmd(cmd, temperature, timeout)
        if ret is None:
            return
        ret = float(ret)
        status_boiler[v.DATA_MAX_CH_SETPOINT] = ret
        self.status.submit_partial_update(v.BOILER, status_boiler)
        return ret

    async def set_dhw_setpoint(self, temperature, timeout=v.OTGW_DEFAULT_TIMEOUT):
        """
        Set the domestic hot water setpoint. This command is only
        available with boilers that support this function.
        Return the newly accepted setpoint, or None on failure.

        This method is a coroutine
        """
        cmd = v.OTGW_CMD_SET_WATER
        status_boiler = {}
        ret = await self._wait_for_cmd(cmd, temperature, timeout)
        if ret is None:
            return
        ret = float(ret)
        status_boiler[v.DATA_DHW_SETPOINT] = ret
        self.status.submit_partial_update(v.BOILER, status_boiler)
        return ret

    async def set_max_relative_mod(self, max_mod, timeout=v.OTGW_DEFAULT_TIMEOUT):
        """
        Override the maximum relative modulation from the thermostat.
        Valid values are 0 through 100. Clear the setting by specifying
        a non-numeric value.
        Return the newly accepted value, '-' if a previous value was
        cleared, or None on failure.

        This method is a coroutine
        """
        if isinstance(max_mod, int) and not 0 <= max_mod <= 100:
            return None
        cmd = v.OTGW_CMD_MAX_MOD
        status_boiler = {}
        ret = await self._wait_for_cmd(cmd, max_mod, timeout)
        if ret is None:
            return
        if ret == "-":
            status_boiler[v.DATA_SLAVE_MAX_RELATIVE_MOD] = None
        else:
            ret = int(ret)
            status_boiler[v.DATA_SLAVE_MAX_RELATIVE_MOD] = ret
        self.status.submit_partial_update(v.BOILER, status_boiler)
        return ret

    async def set_control_setpoint(self, setpoint, timeout=v.OTGW_DEFAULT_TIMEOUT):
        """
        Manipulate the control setpoint being sent to the boiler. Set
        to 0 to pass along the value specified by the thermostat.
        Return the newly accepted value, or None on failure.

        This method is a coroutine
        """
        cmd = v.OTGW_CMD_CONTROL_SETPOINT
        status_boiler = {}
        ret = await self._wait_for_cmd(cmd, setpoint, timeout)
        if ret is None:
            return
        ret = float(ret)
        status_boiler[v.DATA_CONTROL_SETPOINT] = ret
        self.status.submit_partial_update(v.BOILER, status_boiler)
        return ret

    async def set_control_setpoint_2(self, setpoint, timeout=v.OTGW_DEFAULT_TIMEOUT):
        """
        Manipulate the control setpoint being sent to the boiler for the second
        heating circuit. Set to 0 to pass along the value specified by the thermostat.
        Return the newly accepted value, or None on failure.

        This method is a coroutine
        """
        cmd = v.OTGW_CMD_CONTROL_SETPOINT_2
        status_boiler = {}
        ret = await self._wait_for_cmd(cmd, setpoint, timeout)
        if ret is None:
            return
        ret = float(ret)
        status_boiler[v.DATA_CONTROL_SETPOINT_2] = ret
        self.status.submit_partial_update(v.BOILER, status_boiler)
        return ret

    async def set_ch_enable_bit(self, ch_bit, timeout=v.OTGW_DEFAULT_TIMEOUT):
        """
        Control the CH enable status bit when overriding the control
        setpoint. By default the CH enable bit is set after a call to
        set_control_setpoint with a value other than 0. With this
        method, the bit can be manipulated.
        @ch_bit can be either 0 or 1.
        Return the newly accepted value (0 or 1), or None on failure.

        This method is a coroutine
        """
        if ch_bit not in [0, 1]:
            return None
        cmd = v.OTGW_CMD_CONTROL_HEATING
        status_boiler = {}
        ret = await self._wait_for_cmd(cmd, ch_bit, timeout)
        if ret is None:
            return
        ret = int(ret)
        status_boiler[v.DATA_MASTER_CH_ENABLED] = ret
        self.status.submit_partial_update(v.BOILER, status_boiler)
        return ret

    async def set_ch2_enable_bit(self, ch_bit, timeout=v.OTGW_DEFAULT_TIMEOUT):
        """
        Control the CH enable status bit when overriding the control
        setpoint. By default the CH enable bit is set after a call to
        set_control_setpoint with a value other than 0. With this
        method, the bit can be manipulated.
        @ch_bit can be either 0 or 1.
        Return the newly accepted value (0 or 1), or None on failure.

        This method is a coroutine
        """
        if ch_bit not in [0, 1]:
            return None
        cmd = v.OTGW_CMD_CONTROL_HEATING_2
        status_boiler = {}
        ret = await self._wait_for_cmd(cmd, ch_bit, timeout)
        if ret is None:
            return
        ret = int(ret)
        status_boiler[v.DATA_MASTER_CH2_ENABLED] = ret
        self.status.submit_partial_update(v.BOILER, status_boiler)
        return ret

    async def set_ventilation(self, pct, timeout=v.OTGW_DEFAULT_TIMEOUT):
        """
        Configure a ventilation setpoint override value (0-100%).
        Return the newly accepted value, or None on failure.
        @pct :int: Must be between 0 and 100.

        This method is a coroutine
        """
        if not 0 <= pct <= 100:
            return None
        cmd = v.OTGW_CMD_VENT
        status_boiler = {}
        ret = await self._wait_for_cmd(cmd, pct, timeout)
        if ret is None:
            return
        ret = int(ret)
        status_boiler[v.DATA_COOLING_CONTROL] = ret
        self.status.submit_partial_update(v.BOILER, status_boiler)
        return ret

    async def send_transparent_command(
        self, cmd, state, timeout=v.OTGW_DEFAULT_TIMEOUT
    ):
        """
        Sends custom otgw commands throug a transparent interface.
        Check https://otgw.tclcode.com/firmware.html for supported commands.
        @cmd the supported command e.g. 'SC' (set time/day)
        @state the command argument e.g. '23:59/4' (the current time/day)
        Returns the gateway response, which should be equal to @state.

        Note that unlike the set_* methods, this does not update the status object.

        This method is a coroutine
        """
        ret = await self._wait_for_cmd(cmd, state, timeout)
        return ret

    def subscribe(self, coro):
        """
        Subscribe to status updates from the Opentherm Gateway.
        Can only be used after connect()
        @coro is a coroutine which will be called with a single
        argument (status) when a status change occurs.
        Return True on success, False if not connected or already
        subscribed.
        """
        return self.status.subscribe(coro)

    def unsubscribe(self, coro):
        """
        Unsubscribe from status updates from the Opentherm Gateway.
        Can only be used after connect()
        @coro is a coroutine which has been subscribed with subscribe()
        earlier.
        Return True on success, false if not connected or subscribed.
        """
        return self.status.unsubscribe(coro)

    async def _wait_for_cmd(self, cmd, value, timeout=v.OTGW_DEFAULT_TIMEOUT):
        """
        Wrap @cmd in applicable asyncio call.

        This method is a coroutine.
        """
        if not self.connection.connected:
            return None
        try:
            return await asyncio.wait_for(
                self._protocol.command_processor.issue_cmd(cmd, value),
                timeout,
            )
        except asyncio.TimeoutError:
            _LOGGER.error("Timed out waiting for command: %s, value: %s.", cmd, value)
            return
        except (RuntimeError, SyntaxError, ValueError) as exc:
            _LOGGER.error(
                "Command %s with value %s raised exception: %s", cmd, value, exc
            )

    async def _poll_gpio(self, interval=10):
        """
        Start or stop polling GPIO states.

        GPIO states aren't being pushed by the gateway, we need to poll
        if we want updates.
        """
        poll = 0 in (
            self.status.status[v.OTGW].get(v.OTGW_GPIO_A),
            self.status.status[v.OTGW].get(v.OTGW_GPIO_B),
        )
        if poll and self._gpio_task is None:

            async def polling_routine():
                """Poll GPIO state every @interval seconds."""
                try:
                    while True:
                        ret = await self._wait_for_cmd(
                            v.OTGW_CMD_REPORT, v.OTGW_REPORT_GPIO_STATES
                        )
                        if ret:
                            pios = ret[2:]
                            status_otgw = {
                                v.OTGW_GPIO_A_STATE: int(pios[0]),
                                v.OTGW_GPIO_B_STATE: int(pios[1]),
                            }
                            self.status.submit_partial_update(v.OTGW, status_otgw)
                        await asyncio.sleep(interval)
                except asyncio.CancelledError:
                    status_otgw = {
                        v.OTGW_GPIO_A_STATE: 0,
                        v.OTGW_GPIO_B_STATE: 0,
                    }
                    self.status.submit_partial_update(v.OTGW, status_otgw)
                    self._gpio_task = None
                    _LOGGER.debug("GPIO polling routine stopped")

            _LOGGER.debug("Starting GPIO polling routine")
            self._gpio_task = asyncio.get_running_loop().create_task(polling_routine())
        elif not poll and self._gpio_task is not None:
            _LOGGER.debug("Stopping GPIO polling routine")
            self._gpio_task.cancel()


def process_statusfields_v4(status_fields):
    """
    Process the fields of a split status line for OpenTherm Gateway firmware
    version <5.0.
    
    Return a tuple of (boiler_status, thermostat_status).
    """
    device_status = status_fields[0].split("/")
    master_status = device_status[0]
    slave_status = device_status[1]
    remote_params = status_fields[2].split("/")
    capmodlimits = status_fields[4].split("/")
    dhw_setp_bounds = status_fields[13].split("/")
    ch_setp_bounds = status_fields[14].split("/")
    thermostat_status = {
        v.DATA_MASTER_CH_ENABLED: int(master_status[7]),
        v.DATA_MASTER_DHW_ENABLED: int(master_status[6]),
        v.DATA_MASTER_COOLING_ENABLED: int(master_status[5]),
        v.DATA_MASTER_OTC_ENABLED: int(master_status[4]),
        v.DATA_MASTER_CH2_ENABLED: int(master_status[3]),
        v.DATA_CONTROL_SETPOINT: float(status_fields[1]),
        v.DATA_ROOM_SETPOINT: float(status_fields[5]),
        v.DATA_ROOM_TEMP: float(status_fields[8]),
    }
    boiler_status = {
        v.DATA_SLAVE_FAULT_IND: int(slave_status[7]),
        v.DATA_SLAVE_CH_ACTIVE: int(slave_status[6]),
        v.DATA_SLAVE_DHW_ACTIVE: int(slave_status[5]),
        v.DATA_SLAVE_FLAME_ON: int(slave_status[4]),
        v.DATA_SLAVE_COOLING_ACTIVE: int(slave_status[3]),
        v.DATA_SLAVE_CH2_ACTIVE: int(slave_status[2]),
        v.DATA_SLAVE_DIAG_IND: int(slave_status[1]),
        v.DATA_REMOTE_TRANSFER_DHW: int(remote_params[0][7]),
        v.DATA_REMOTE_TRANSFER_MAX_CH: int(remote_params[0][6]),
        v.DATA_REMOTE_RW_DHW: int(remote_params[1][7]),
        v.DATA_REMOTE_RW_MAX_CH: int(remote_params[1][6]),
        v.DATA_SLAVE_MAX_RELATIVE_MOD: float(status_fields[3]),
        v.DATA_SLAVE_MAX_CAPACITY: int(capmodlimits[0]),
        v.DATA_SLAVE_MIN_MOD_LEVEL: int(capmodlimits[1]),
        v.DATA_REL_MOD_LEVEL: float(status_fields[6]),
        v.DATA_CH_WATER_PRESS: float(status_fields[7]),
        v.DATA_CH_WATER_TEMP: float(status_fields[9]),
        v.DATA_DHW_TEMP: float(status_fields[10]),
        v.DATA_OUTSIDE_TEMP: float(status_fields[11]),
        v.DATA_RETURN_WATER_TEMP: float(status_fields[12]),
        v.DATA_SLAVE_DHW_MAX_SETP: int(dhw_setp_bounds[0]),
        v.DATA_SLAVE_DHW_MIN_SETP: int(dhw_setp_bounds[1]),
        v.DATA_SLAVE_CH_MAX_SETP: int(ch_setp_bounds[0]),
        v.DATA_SLAVE_CH_MIN_SETP: int(ch_setp_bounds[1]),
        v.DATA_DHW_SETPOINT: float(status_fields[15]),
        v.DATA_MAX_CH_SETPOINT: float(status_fields[16]),
        v.DATA_TOTAL_BURNER_STARTS: int(status_fields[17]),
        v.DATA_CH_PUMP_STARTS: int(status_fields[18]),
        v.DATA_DHW_PUMP_STARTS: int(status_fields[19]),
        v.DATA_DHW_BURNER_STARTS: int(status_fields[20]),
        v.DATA_TOTAL_BURNER_HOURS: int(status_fields[21]),
        v.DATA_CH_PUMP_HOURS: int(status_fields[22]),
        v.DATA_DHW_PUMP_HOURS: int(status_fields[23]),
        v.DATA_DHW_BURNER_HOURS: int(status_fields[24]),
    }
    return (boiler_status, thermostat_status)


def process_statusfields_v5(status_fields):
    """
    Process the fields of a split status line for OpenTherm Gateway firmware
    version >=5.0.
    
    Return a tuple of (boiler_status, thermostat_status).
    """
    device_status = status_fields[0].split("/")
    master_status = device_status[0]
    slave_status = device_status[1]
    remote_params = status_fields[2].split("/")
    capmodlimits = status_fields[6].split("/")
    dhw_setp_bounds = status_fields[19].split("/")
    ch_setp_bounds = status_fields[20].split("/")
    vh_device_status = status_fields[23].split("/")
    vh_master_status = vh_device_status[0]
    vh_slave_status = vh_device_status[1]
    thermostat_status = {
        v.DATA_MASTER_CH_ENABLED: int(master_status[7]),
        v.DATA_MASTER_DHW_ENABLED: int(master_status[6]),
        v.DATA_MASTER_COOLING_ENABLED: int(master_status[5]),
        v.DATA_MASTER_OTC_ENABLED: int(master_status[4]),
        v.DATA_MASTER_CH2_ENABLED: int(master_status[3]),
        v.DATA_CONTROL_SETPOINT: float(status_fields[1]),
        v.DATA_ROOM_SETPOINT: float(status_fields[7]),
        v.DATA_COOLING_CONTROL: float(status_fields[3]),
        v.DATA_CONTROL_SETPOINT_2: float(status_fields[4]),
        v.DATA_ROOM_SETPOINT_2: float(status_fields[11]),
        v.DATA_ROOM_TEMP: float(status_fields[12]),
        v.DATA_VH_MASTER_VENT_ENABLED: int(vh_master_status[7]),
        v.DATA_VH_MASTER_BYPASS_POS: int(vh_master_status[6]),
        v.DATA_VH_MASTER_BYPASS_MODE: int(vh_master_status[5]),
        v.DATA_VH_MASTER_FREE_VENT_MODE: int(vh_master_status[4]),
        v.DATA_VH_CONTROL_SETPOINT: int(status_fields[24]),
    }
    boiler_status = {
        v.DATA_SLAVE_FAULT_IND: int(slave_status[7]),
        v.DATA_SLAVE_CH_ACTIVE: int(slave_status[6]),
        v.DATA_SLAVE_DHW_ACTIVE: int(slave_status[5]),
        v.DATA_SLAVE_FLAME_ON: int(slave_status[4]),
        v.DATA_SLAVE_COOLING_ACTIVE: int(slave_status[3]),
        v.DATA_SLAVE_CH2_ACTIVE: int(slave_status[2]),
        v.DATA_SLAVE_DIAG_IND: int(slave_status[1]),
        v.DATA_REMOTE_TRANSFER_DHW: int(remote_params[0][7]),
        v.DATA_REMOTE_TRANSFER_MAX_CH: int(remote_params[0][6]),
        v.DATA_REMOTE_RW_DHW: int(remote_params[1][7]),
        v.DATA_REMOTE_RW_MAX_CH: int(remote_params[1][6]),
        v.DATA_SLAVE_MAX_RELATIVE_MOD: float(status_fields[5]),
        v.DATA_SLAVE_MAX_CAPACITY: int(capmodlimits[0]),
        v.DATA_SLAVE_MIN_MOD_LEVEL: int(capmodlimits[1]),
        v.DATA_REL_MOD_LEVEL: float(status_fields[8]),
        v.DATA_CH_WATER_PRESS: float(status_fields[9]),
        v.DATA_DHW_FLOW_RATE: float(status_fields[10]),
        v.DATA_CH_WATER_TEMP: float(status_fields[13]),
        v.DATA_DHW_TEMP: float(status_fields[14]),
        v.DATA_OUTSIDE_TEMP: float(status_fields[15]),
        v.DATA_RETURN_WATER_TEMP: float(status_fields[16]),
        v.DATA_CH_WATER_TEMP_2: float(status_fields[17]),
        v.DATA_EXHAUST_TEMP: int(status_fields[18]),
        v.DATA_SLAVE_DHW_MAX_SETP: int(dhw_setp_bounds[0]),
        v.DATA_SLAVE_DHW_MIN_SETP: int(dhw_setp_bounds[1]),
        v.DATA_SLAVE_CH_MAX_SETP: int(ch_setp_bounds[0]),
        v.DATA_SLAVE_CH_MIN_SETP: int(ch_setp_bounds[1]),
        v.DATA_DHW_SETPOINT: float(status_fields[21]),
        v.DATA_MAX_CH_SETPOINT: float(status_fields[22]),
        v.DATA_VH_SLAVE_FAULT_INDICATE: int(vh_slave_status[7]),
        v.DATA_VH_SLAVE_VENT_MODE: int(vh_slave_status[6]),
        v.DATA_VH_SLAVE_BYPASS_STATUS: int(vh_slave_status[5]),
        v.DATA_VH_SLAVE_BYPASS_AUTO_STATUS: int(vh_slave_status[4]),
        v.DATA_VH_SLAVE_FREE_VENT_STATUS: int(vh_slave_status[3]),
        v.DATA_VH_SLAVE_DIAG_INDICATE: int(vh_slave_status[1]),
        v.DATA_TOTAL_BURNER_STARTS: int(status_fields[26]),
        v.DATA_CH_PUMP_STARTS: int(status_fields[27]),
        v.DATA_DHW_PUMP_STARTS: int(status_fields[28]),
        v.DATA_DHW_BURNER_STARTS: int(status_fields[29]),
        v.DATA_TOTAL_BURNER_HOURS: int(status_fields[30]),
        v.DATA_CH_PUMP_HOURS: int(status_fields[31]),
        v.DATA_DHW_PUMP_HOURS: int(status_fields[32]),
        v.DATA_DHW_BURNER_HOURS: int(status_fields[33]),
        v.DATA_VH_RELATIVE_VENT: int(status_fields[25]),
    }
    return (boiler_status, thermostat_status)
