"""Support for INSTEON Modems (PLM and Hub)."""
import collections
import logging
from typing import Dict

import insteonplm
from insteonplm.devices import ALDBStatus
from insteonplm.states.cover import Cover
from insteonplm.states.dimmable import (
    DimmableKeypadA,
    DimmableRemote,
    DimmableSwitch,
    DimmableSwitch_Fan,
)
from insteonplm.states.onOff import (
    OnOffKeypad,
    OnOffKeypadA,
    OnOffSwitch,
    OnOffSwitch_OutletBottom,
    OnOffSwitch_OutletTop,
    OpenClosedRelay,
)
from insteonplm.states.sensor import (
    IoLincSensor,
    LeakSensorDryWet,
    OnOffSensor,
    SmokeCO2Sensor,
    VariableSensor,
)
from insteonplm.states.x10 import (
    X10AllLightsOffSensor,
    X10AllLightsOnSensor,
    X10AllUnitsOffSensor,
    X10DimmableSwitch,
    X10OnOffSensor,
    X10OnOffSwitch,
)
import voluptuous as vol

from homeassistant.const import (
    CONF_ADDRESS,
    CONF_ENTITY_ID,
    CONF_HOST,
    CONF_PLATFORM,
    CONF_PORT,
    ENTITY_MATCH_ALL,
    EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.core import callback
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send
from homeassistant.helpers.entity import Entity

_LOGGER = logging.getLogger(__name__)

DOMAIN = "insteon"
INSTEON_ENTITIES = "entities"

CONF_IP_PORT = "ip_port"
CONF_HUB_USERNAME = "username"
CONF_HUB_PASSWORD = "password"
CONF_HUB_VERSION = "hub_version"
CONF_OVERRIDE = "device_override"
CONF_PLM_HUB_MSG = "Must configure either a PLM port or a Hub host"
CONF_CAT = "cat"
CONF_SUBCAT = "subcat"
CONF_FIRMWARE = "firmware"
CONF_PRODUCT_KEY = "product_key"
CONF_X10 = "x10_devices"
CONF_HOUSECODE = "housecode"
CONF_UNITCODE = "unitcode"
CONF_DIM_STEPS = "dim_steps"
CONF_X10_ALL_UNITS_OFF = "x10_all_units_off"
CONF_X10_ALL_LIGHTS_ON = "x10_all_lights_on"
CONF_X10_ALL_LIGHTS_OFF = "x10_all_lights_off"

SRV_ADD_ALL_LINK = "add_all_link"
SRV_DEL_ALL_LINK = "delete_all_link"
SRV_LOAD_ALDB = "load_all_link_database"
SRV_PRINT_ALDB = "print_all_link_database"
SRV_PRINT_IM_ALDB = "print_im_all_link_database"
SRV_X10_ALL_UNITS_OFF = "x10_all_units_off"
SRV_X10_ALL_LIGHTS_OFF = "x10_all_lights_off"
SRV_X10_ALL_LIGHTS_ON = "x10_all_lights_on"
SRV_ALL_LINK_GROUP = "group"
SRV_ALL_LINK_MODE = "mode"
SRV_LOAD_DB_RELOAD = "reload"
SRV_CONTROLLER = "controller"
SRV_RESPONDER = "responder"
SRV_HOUSECODE = "housecode"
SRV_SCENE_ON = "scene_on"
SRV_SCENE_OFF = "scene_off"

SIGNAL_LOAD_ALDB = "load_aldb"
SIGNAL_PRINT_ALDB = "print_aldb"

HOUSECODES = [
    "a",
    "b",
    "c",
    "d",
    "e",
    "f",
    "g",
    "h",
    "i",
    "j",
    "k",
    "l",
    "m",
    "n",
    "o",
    "p",
]

BUTTON_PRESSED_STATE_NAME = "onLevelButton"
EVENT_BUTTON_ON = "insteon.button_on"
EVENT_BUTTON_OFF = "insteon.button_off"
EVENT_CONF_BUTTON = "button"


def set_default_port(schema: Dict) -> Dict:
    """Set the default port based on the Hub version."""
    # If the ip_port is found do nothing
    # If it is not found the set the default
    ip_port = schema.get(CONF_IP_PORT)
    if not ip_port:
        hub_version = schema.get(CONF_HUB_VERSION)
        # Found hub_version but not ip_port
        if hub_version == 1:
            schema[CONF_IP_PORT] = 9761
        else:
            schema[CONF_IP_PORT] = 25105
    return schema


CONF_DEVICE_OVERRIDE_SCHEMA = vol.All(
    cv.deprecated(CONF_PLATFORM),
    vol.Schema(
        {
            vol.Required(CONF_ADDRESS): cv.string,
            vol.Optional(CONF_CAT): cv.byte,
            vol.Optional(CONF_SUBCAT): cv.byte,
            vol.Optional(CONF_FIRMWARE): cv.byte,
            vol.Optional(CONF_PRODUCT_KEY): cv.byte,
            vol.Optional(CONF_PLATFORM): cv.string,
        }
    ),
)


CONF_X10_SCHEMA = vol.All(
    vol.Schema(
        {
            vol.Required(CONF_HOUSECODE): cv.string,
            vol.Required(CONF_UNITCODE): vol.Range(min=1, max=16),
            vol.Required(CONF_PLATFORM): cv.string,
            vol.Optional(CONF_DIM_STEPS): vol.Range(min=2, max=255),
        }
    )
)


CONFIG_SCHEMA = vol.Schema(
    {
        DOMAIN: vol.All(
            vol.Schema(
                {
                    vol.Exclusive(
                        CONF_PORT, "plm_or_hub", msg=CONF_PLM_HUB_MSG
                    ): cv.string,
                    vol.Exclusive(
                        CONF_HOST, "plm_or_hub", msg=CONF_PLM_HUB_MSG
                    ): cv.string,
                    vol.Optional(CONF_IP_PORT): cv.port,
                    vol.Optional(CONF_HUB_USERNAME): cv.string,
                    vol.Optional(CONF_HUB_PASSWORD): cv.string,
                    vol.Optional(CONF_HUB_VERSION, default=2): vol.In([1, 2]),
                    vol.Optional(CONF_OVERRIDE): vol.All(
                        cv.ensure_list_csv, [CONF_DEVICE_OVERRIDE_SCHEMA]
                    ),
                    vol.Optional(CONF_X10_ALL_UNITS_OFF): vol.In(HOUSECODES),
                    vol.Optional(CONF_X10_ALL_LIGHTS_ON): vol.In(HOUSECODES),
                    vol.Optional(CONF_X10_ALL_LIGHTS_OFF): vol.In(HOUSECODES),
                    vol.Optional(CONF_X10): vol.All(
                        cv.ensure_list_csv, [CONF_X10_SCHEMA]
                    ),
                },
                extra=vol.ALLOW_EXTRA,
                required=True,
            ),
            cv.has_at_least_one_key(CONF_PORT, CONF_HOST),
            set_default_port,
        )
    },
    extra=vol.ALLOW_EXTRA,
)


ADD_ALL_LINK_SCHEMA = vol.Schema(
    {
        vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255),
        vol.Required(SRV_ALL_LINK_MODE): vol.In([SRV_CONTROLLER, SRV_RESPONDER]),
    }
)


DEL_ALL_LINK_SCHEMA = vol.Schema(
    {vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255)}
)


LOAD_ALDB_SCHEMA = vol.Schema(
    {
        vol.Required(CONF_ENTITY_ID): vol.Any(cv.entity_id, ENTITY_MATCH_ALL),
        vol.Optional(SRV_LOAD_DB_RELOAD, default=False): cv.boolean,
    }
)


PRINT_ALDB_SCHEMA = vol.Schema({vol.Required(CONF_ENTITY_ID): cv.entity_id})


X10_HOUSECODE_SCHEMA = vol.Schema({vol.Required(SRV_HOUSECODE): vol.In(HOUSECODES)})


TRIGGER_SCENE_SCHEMA = vol.Schema(
    {vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255)}
)


STATE_NAME_LABEL_MAP = {
    "keypadButtonA": "Button A",
    "keypadButtonB": "Button B",
    "keypadButtonC": "Button C",
    "keypadButtonD": "Button D",
    "keypadButtonE": "Button E",
    "keypadButtonF": "Button F",
    "keypadButtonG": "Button G",
    "keypadButtonH": "Button H",
    "keypadButtonMain": "Main",
    "onOffButtonA": "Button A",
    "onOffButtonB": "Button B",
    "onOffButtonC": "Button C",
    "onOffButtonD": "Button D",
    "onOffButtonE": "Button E",
    "onOffButtonF": "Button F",
    "onOffButtonG": "Button G",
    "onOffButtonH": "Button H",
    "onOffButtonMain": "Main",
    "fanOnLevel": "Fan",
    "lightOnLevel": "Light",
    "coolSetPoint": "Cool Set",
    "heatSetPoint": "HeatSet",
    "statusReport": "Status",
    "generalSensor": "Sensor",
    "motionSensor": "Motion",
    "lightSensor": "Light",
    "batterySensor": "Battery",
    "dryLeakSensor": "Dry",
    "wetLeakSensor": "Wet",
    "heartbeatLeakSensor": "Heartbeat",
    "openClosedRelay": "Relay",
    "openClosedSensor": "Sensor",
    "lightOnOff": "Light",
    "outletTopOnOff": "Top",
    "outletBottomOnOff": "Bottom",
    "coverOpenLevel": "Cover",
}


async def async_setup(hass, config):
    """Set up the connection to the modem."""
    ipdb = IPDB()
    insteon_modem = None

    conf = config[DOMAIN]
    port = conf.get(CONF_PORT)
    host = conf.get(CONF_HOST)
    ip_port = conf.get(CONF_IP_PORT)
    username = conf.get(CONF_HUB_USERNAME)
    password = conf.get(CONF_HUB_PASSWORD)
    hub_version = conf.get(CONF_HUB_VERSION)
    overrides = conf.get(CONF_OVERRIDE, [])
    x10_devices = conf.get(CONF_X10, [])
    x10_all_units_off_housecode = conf.get(CONF_X10_ALL_UNITS_OFF)
    x10_all_lights_on_housecode = conf.get(CONF_X10_ALL_LIGHTS_ON)
    x10_all_lights_off_housecode = conf.get(CONF_X10_ALL_LIGHTS_OFF)

    @callback
    def async_new_insteon_device(device):
        """Detect device from transport to be delegated to platform."""
        for state_key in device.states:
            platform_info = ipdb[device.states[state_key]]
            if platform_info and platform_info.platform:
                platform = platform_info.platform

                if platform == "on_off_events":
                    device.states[state_key].register_updates(_fire_button_on_off_event)

                else:
                    _LOGGER.info(
                        "New INSTEON device: %s (%s) %s",
                        device.address,
                        device.states[state_key].name,
                        platform,
                    )

                    hass.async_create_task(
                        discovery.async_load_platform(
                            hass,
                            platform,
                            DOMAIN,
                            discovered={
                                "address": device.address.id,
                                "state_key": state_key,
                            },
                            hass_config=config,
                        )
                    )

    def add_all_link(service):
        """Add an INSTEON All-Link between two devices."""
        group = service.data.get(SRV_ALL_LINK_GROUP)
        mode = service.data.get(SRV_ALL_LINK_MODE)
        link_mode = 1 if mode.lower() == SRV_CONTROLLER else 0
        insteon_modem.start_all_linking(link_mode, group)

    def del_all_link(service):
        """Delete an INSTEON All-Link between two devices."""
        group = service.data.get(SRV_ALL_LINK_GROUP)
        insteon_modem.start_all_linking(255, group)

    def load_aldb(service):
        """Load the device All-Link database."""
        entity_id = service.data[CONF_ENTITY_ID]
        reload = service.data[SRV_LOAD_DB_RELOAD]
        if entity_id.lower() == ENTITY_MATCH_ALL:
            for entity_id in hass.data[DOMAIN].get(INSTEON_ENTITIES):
                _send_load_aldb_signal(entity_id, reload)
        else:
            _send_load_aldb_signal(entity_id, reload)

    def _send_load_aldb_signal(entity_id, reload):
        """Send the load All-Link database signal to INSTEON entity."""
        signal = f"{entity_id}_{SIGNAL_LOAD_ALDB}"
        dispatcher_send(hass, signal, reload)

    def print_aldb(service):
        """Print the All-Link Database for a device."""
        # For now this sends logs to the log file.
        # Furture direction is to create an INSTEON control panel.
        entity_id = service.data[CONF_ENTITY_ID]
        signal = f"{entity_id}_{SIGNAL_PRINT_ALDB}"
        dispatcher_send(hass, signal)

    def print_im_aldb(service):
        """Print the All-Link Database for a device."""
        # For now this sends logs to the log file.
        # Furture direction is to create an INSTEON control panel.
        print_aldb_to_log(insteon_modem.aldb)

    def x10_all_units_off(service):
        """Send the X10 All Units Off command."""
        housecode = service.data.get(SRV_HOUSECODE)
        insteon_modem.x10_all_units_off(housecode)

    def x10_all_lights_off(service):
        """Send the X10 All Lights Off command."""
        housecode = service.data.get(SRV_HOUSECODE)
        insteon_modem.x10_all_lights_off(housecode)

    def x10_all_lights_on(service):
        """Send the X10 All Lights On command."""
        housecode = service.data.get(SRV_HOUSECODE)
        insteon_modem.x10_all_lights_on(housecode)

    def scene_on(service):
        """Trigger an INSTEON scene ON."""
        group = service.data.get(SRV_ALL_LINK_GROUP)
        insteon_modem.trigger_group_on(group)

    def scene_off(service):
        """Trigger an INSTEON scene ON."""
        group = service.data.get(SRV_ALL_LINK_GROUP)
        insteon_modem.trigger_group_off(group)

    def _register_services():
        hass.services.register(
            DOMAIN, SRV_ADD_ALL_LINK, add_all_link, schema=ADD_ALL_LINK_SCHEMA
        )
        hass.services.register(
            DOMAIN, SRV_DEL_ALL_LINK, del_all_link, schema=DEL_ALL_LINK_SCHEMA
        )
        hass.services.register(
            DOMAIN, SRV_LOAD_ALDB, load_aldb, schema=LOAD_ALDB_SCHEMA
        )
        hass.services.register(
            DOMAIN, SRV_PRINT_ALDB, print_aldb, schema=PRINT_ALDB_SCHEMA
        )
        hass.services.register(DOMAIN, SRV_PRINT_IM_ALDB, print_im_aldb, schema=None)
        hass.services.register(
            DOMAIN,
            SRV_X10_ALL_UNITS_OFF,
            x10_all_units_off,
            schema=X10_HOUSECODE_SCHEMA,
        )
        hass.services.register(
            DOMAIN,
            SRV_X10_ALL_LIGHTS_OFF,
            x10_all_lights_off,
            schema=X10_HOUSECODE_SCHEMA,
        )
        hass.services.register(
            DOMAIN,
            SRV_X10_ALL_LIGHTS_ON,
            x10_all_lights_on,
            schema=X10_HOUSECODE_SCHEMA,
        )
        hass.services.register(
            DOMAIN, SRV_SCENE_ON, scene_on, schema=TRIGGER_SCENE_SCHEMA
        )
        hass.services.register(
            DOMAIN, SRV_SCENE_OFF, scene_off, schema=TRIGGER_SCENE_SCHEMA
        )
        _LOGGER.debug("Insteon Services registered")

    def _fire_button_on_off_event(address, group, val):
        # Firing an event when a button is pressed.
        device = insteon_modem.devices[address.hex]
        state_name = device.states[group].name
        button = (
            "" if state_name == BUTTON_PRESSED_STATE_NAME else state_name[-1].lower()
        )
        schema = {CONF_ADDRESS: address.hex}
        if button != "":
            schema[EVENT_CONF_BUTTON] = button
        if val:
            event = EVENT_BUTTON_ON
        else:
            event = EVENT_BUTTON_OFF
        _LOGGER.debug(
            "Firing event %s with address %s and button %s", event, address.hex, button
        )
        hass.bus.fire(event, schema)

    if host:
        _LOGGER.info("Connecting to Insteon Hub on %s", host)
        conn = await insteonplm.Connection.create(
            host=host,
            port=ip_port,
            username=username,
            password=password,
            hub_version=hub_version,
            loop=hass.loop,
            workdir=hass.config.config_dir,
        )
    else:
        _LOGGER.info("Looking for Insteon PLM on %s", port)
        conn = await insteonplm.Connection.create(
            device=port, loop=hass.loop, workdir=hass.config.config_dir
        )

    insteon_modem = conn.protocol

    for device_override in overrides:
        #
        # Override the device default capabilities for a specific address
        #
        address = device_override.get("address")
        for prop in device_override:
            if prop in [CONF_CAT, CONF_SUBCAT]:
                insteon_modem.devices.add_override(address, prop, device_override[prop])
            elif prop in [CONF_FIRMWARE, CONF_PRODUCT_KEY]:
                insteon_modem.devices.add_override(
                    address, CONF_PRODUCT_KEY, device_override[prop]
                )

    hass.data[DOMAIN] = {}
    hass.data[DOMAIN]["modem"] = insteon_modem
    hass.data[DOMAIN][INSTEON_ENTITIES] = {}

    hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, conn.close)

    insteon_modem.devices.add_device_callback(async_new_insteon_device)

    if x10_all_units_off_housecode:
        device = insteon_modem.add_x10_device(
            x10_all_units_off_housecode, 20, "allunitsoff"
        )
    if x10_all_lights_on_housecode:
        device = insteon_modem.add_x10_device(
            x10_all_lights_on_housecode, 21, "alllightson"
        )
    if x10_all_lights_off_housecode:
        device = insteon_modem.add_x10_device(
            x10_all_lights_off_housecode, 22, "alllightsoff"
        )
    for device in x10_devices:
        housecode = device.get(CONF_HOUSECODE)
        unitcode = device.get(CONF_UNITCODE)
        x10_type = "onoff"
        steps = device.get(CONF_DIM_STEPS, 22)
        if device.get(CONF_PLATFORM) == "light":
            x10_type = "dimmable"
        elif device.get(CONF_PLATFORM) == "binary_sensor":
            x10_type = "sensor"
        _LOGGER.debug(
            "Adding X10 device to Insteon: %s %d %s", housecode, unitcode, x10_type
        )
        device = insteon_modem.add_x10_device(housecode, unitcode, x10_type)
        if device and hasattr(device.states[0x01], "steps"):
            device.states[0x01].steps = steps

    hass.async_add_job(_register_services)

    return True


State = collections.namedtuple("Product", "stateType platform")


class IPDB:
    """Embodies the INSTEON Product Database static data and access methods."""

    def __init__(self):
        """Create the INSTEON Product Database (IPDB)."""
        self.states = [
            State(Cover, "cover"),
            State(OnOffSwitch_OutletTop, "switch"),
            State(OnOffSwitch_OutletBottom, "switch"),
            State(OpenClosedRelay, "switch"),
            State(OnOffSwitch, "switch"),
            State(OnOffKeypadA, "switch"),
            State(OnOffKeypad, "switch"),
            State(LeakSensorDryWet, "binary_sensor"),
            State(IoLincSensor, "binary_sensor"),
            State(SmokeCO2Sensor, "sensor"),
            State(OnOffSensor, "binary_sensor"),
            State(VariableSensor, "sensor"),
            State(DimmableSwitch_Fan, "fan"),
            State(DimmableSwitch, "light"),
            State(DimmableRemote, "on_off_events"),
            State(DimmableKeypadA, "light"),
            State(X10DimmableSwitch, "light"),
            State(X10OnOffSwitch, "switch"),
            State(X10OnOffSensor, "binary_sensor"),
            State(X10AllUnitsOffSensor, "binary_sensor"),
            State(X10AllLightsOnSensor, "binary_sensor"),
            State(X10AllLightsOffSensor, "binary_sensor"),
        ]

    def __len__(self):
        """Return the number of INSTEON state types mapped to HA platforms."""
        return len(self.states)

    def __iter__(self):
        """Itterate through the INSTEON state types to HA platforms."""
        for product in self.states:
            yield product

    def __getitem__(self, key):
        """Return a Home Assistant platform from an INSTEON state type."""
        for state in self.states:
            if isinstance(key, state.stateType):
                return state
        return None


class InsteonEntity(Entity):
    """INSTEON abstract base entity."""

    def __init__(self, device, state_key):
        """Initialize the INSTEON binary sensor."""
        self._insteon_device_state = device.states[state_key]
        self._insteon_device = device
        self._insteon_device.aldb.add_loaded_callback(self._aldb_loaded)

    @property
    def should_poll(self):
        """No polling needed."""
        return False

    @property
    def address(self):
        """Return the address of the node."""
        return self._insteon_device.address.human

    @property
    def group(self):
        """Return the INSTEON group that the entity responds to."""
        return self._insteon_device_state.group

    @property
    def unique_id(self) -> str:
        """Return a unique ID."""
        if self._insteon_device_state.group == 0x01:
            uid = self._insteon_device.id
        else:
            uid = "{:s}_{:d}".format(
                self._insteon_device.id, self._insteon_device_state.group
            )
        return uid

    @property
    def name(self):
        """Return the name of the node (used for Entity_ID)."""
        # Set a base description
        description = self._insteon_device.description
        if self._insteon_device.description is None:
            description = "Unknown Device"

        # Get an extension label if there is one
        extension = self._get_label()
        if extension:
            extension = " " + extension
        name = "{:s} {:s}{:s}".format(
            description, self._insteon_device.address.human, extension
        )
        return name

    @property
    def device_state_attributes(self):
        """Provide attributes for display on device card."""
        attributes = {"INSTEON Address": self.address, "INSTEON Group": self.group}
        return attributes

    @callback
    def async_entity_update(self, deviceid, group, val):
        """Receive notification from transport that new data exists."""
        _LOGGER.debug(
            "Received update for device %s group %d value %s",
            deviceid.human,
            group,
            val,
        )
        self.async_schedule_update_ha_state()

    async def async_added_to_hass(self):
        """Register INSTEON update events."""
        _LOGGER.debug(
            "Tracking updates for device %s group %d statename %s",
            self.address,
            self.group,
            self._insteon_device_state.name,
        )
        self._insteon_device_state.register_updates(self.async_entity_update)
        self.hass.data[DOMAIN][INSTEON_ENTITIES][self.entity_id] = self
        load_signal = f"{self.entity_id}_{SIGNAL_LOAD_ALDB}"
        async_dispatcher_connect(self.hass, load_signal, self._load_aldb)
        print_signal = f"{self.entity_id}_{SIGNAL_PRINT_ALDB}"
        async_dispatcher_connect(self.hass, print_signal, self._print_aldb)

    def _load_aldb(self, reload=False):
        """Load the device All-Link Database."""
        if reload:
            self._insteon_device.aldb.clear()
        self._insteon_device.read_aldb()

    def _print_aldb(self):
        """Print the device ALDB to the log file."""
        print_aldb_to_log(self._insteon_device.aldb)

    @callback
    def _aldb_loaded(self):
        """All-Link Database loaded for the device."""
        self._print_aldb()

    def _get_label(self):
        """Get the device label for grouped devices."""
        label = ""
        if len(self._insteon_device.states) > 1:
            if self._insteon_device_state.name in STATE_NAME_LABEL_MAP:
                label = STATE_NAME_LABEL_MAP[self._insteon_device_state.name]
            else:
                label = f"Group {self.group:d}"
        return label


def print_aldb_to_log(aldb):
    """Print the All-Link Database to the log file."""
    _LOGGER.info("ALDB load status is %s", aldb.status.name)
    if aldb.status not in [ALDBStatus.LOADED, ALDBStatus.PARTIAL]:
        _LOGGER.warning("Device All-Link database not loaded")
        _LOGGER.warning("Use service insteon.load_aldb first")
        return

    _LOGGER.info("RecID In Use Mode HWM Group Address  Data 1 Data 2 Data 3")
    _LOGGER.info("----- ------ ---- --- ----- -------- ------ ------ ------")
    for mem_addr in aldb:
        rec = aldb[mem_addr]
        # For now we write this to the log
        # Roadmap is to create a configuration panel
        in_use = "Y" if rec.control_flags.is_in_use else "N"
        mode = "C" if rec.control_flags.is_controller else "R"
        hwm = "Y" if rec.control_flags.is_high_water_mark else "N"
        _LOGGER.info(
            " {:04x}    {:s}     {:s}   {:s}    {:3d} {:s}"
            "   {:3d}   {:3d}   {:3d}".format(
                rec.mem_addr,
                in_use,
                mode,
                hwm,
                rec.group,
                rec.address.human,
                rec.data1,
                rec.data2,
                rec.data3,
            )
        )
