Add Internet Printing Protocol (IPP) integration (#32859)
* Create __init__.py * Create manifest.json * Update zeroconf.py * more work on integration * more work on integration. * add more sensor tests. * Update const.py * Update sensor.py * more work on ipp. * Update test_config_flow.py * more work on ipp. * Update config_flow.py * Update config_flow.py
This commit is contained in:
parent
0e3c1dc031
commit
98f68f4798
17 changed files with 1165 additions and 0 deletions
|
@ -187,6 +187,7 @@ homeassistant/components/intesishome/* @jnimmo
|
|||
homeassistant/components/ios/* @robbiet480
|
||||
homeassistant/components/iperf3/* @rohankapoorcom
|
||||
homeassistant/components/ipma/* @dgomes @abmantis
|
||||
homeassistant/components/ipp/* @ctalkington
|
||||
homeassistant/components/iqvia/* @bachya
|
||||
homeassistant/components/irish_rail_transport/* @ttroy50
|
||||
homeassistant/components/izone/* @Swamp-Ig
|
||||
|
|
32
homeassistant/components/ipp/.translations/en.json
Normal file
32
homeassistant/components/ipp/.translations/en.json
Normal file
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "This printer is already configured.",
|
||||
"connection_error": "Failed to connect to printer.",
|
||||
"connection_upgrade": "Failed to connect to printer due to connection upgrade being required."
|
||||
},
|
||||
"error": {
|
||||
"connection_error": "Failed to connect to printer.",
|
||||
"connection_upgrade": "Failed to connect to printer. Please try again with SSL/TLS option checked."
|
||||
},
|
||||
"flow_title": "Printer: {name}",
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"base_path": "Relative path to the printer",
|
||||
"host": "Host or IP address",
|
||||
"port": "Port",
|
||||
"ssl": "Printer supports communication over SSL/TLS",
|
||||
"verify_ssl": "Printer uses a proper SSL certificate"
|
||||
},
|
||||
"description": "Set up your printer via Internet Printing Protocol (IPP) to integrate with Home Assistant.",
|
||||
"title": "Link your printer"
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
"description": "Do you want to add the printer named `{name}` to Home Assistant?",
|
||||
"title": "Discovered printer"
|
||||
}
|
||||
},
|
||||
"title": "Internet Printing Protocol (IPP)"
|
||||
}
|
||||
}
|
190
homeassistant/components/ipp/__init__.py
Normal file
190
homeassistant/components/ipp/__init__.py
Normal file
|
@ -0,0 +1,190 @@
|
|||
"""The Internet Printing Protocol (IPP) integration."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from pyipp import IPP, IPPError, Printer as IPPPrinter
|
||||
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_NAME,
|
||||
CONF_HOST,
|
||||
CONF_PORT,
|
||||
CONF_SSL,
|
||||
CONF_VERIFY_SSL,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import (
|
||||
ATTR_IDENTIFIERS,
|
||||
ATTR_MANUFACTURER,
|
||||
ATTR_MODEL,
|
||||
ATTR_SOFTWARE_VERSION,
|
||||
CONF_BASE_PATH,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
PLATFORMS = [SENSOR_DOMAIN]
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: Dict) -> bool:
|
||||
"""Set up the IPP component."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up IPP from a config entry."""
|
||||
|
||||
# Create IPP instance for this entry
|
||||
coordinator = IPPDataUpdateCoordinator(
|
||||
hass,
|
||||
host=entry.data[CONF_HOST],
|
||||
port=entry.data[CONF_PORT],
|
||||
base_path=entry.data[CONF_BASE_PATH],
|
||||
tls=entry.data[CONF_SSL],
|
||||
verify_ssl=entry.data[CONF_VERIFY_SSL],
|
||||
)
|
||||
await coordinator.async_refresh()
|
||||
|
||||
if not coordinator.last_update_success:
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
|
||||
for component in PLATFORMS:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, component)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = all(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_unload(entry, component)
|
||||
for component in PLATFORMS
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
class IPPDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Class to manage fetching IPP data from single endpoint."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
*,
|
||||
host: str,
|
||||
port: int,
|
||||
base_path: str,
|
||||
tls: bool,
|
||||
verify_ssl: bool,
|
||||
):
|
||||
"""Initialize global IPP data updater."""
|
||||
self.ipp = IPP(
|
||||
host=host,
|
||||
port=port,
|
||||
base_path=base_path,
|
||||
tls=tls,
|
||||
verify_ssl=verify_ssl,
|
||||
session=async_get_clientsession(hass, verify_ssl),
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> IPPPrinter:
|
||||
"""Fetch data from IPP."""
|
||||
try:
|
||||
return await self.ipp.printer()
|
||||
except IPPError as error:
|
||||
raise UpdateFailed(f"Invalid response from API: {error}")
|
||||
|
||||
|
||||
class IPPEntity(Entity):
|
||||
"""Defines a base IPP entity."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
entry_id: str,
|
||||
coordinator: IPPDataUpdateCoordinator,
|
||||
name: str,
|
||||
icon: str,
|
||||
enabled_default: bool = True,
|
||||
) -> None:
|
||||
"""Initialize the IPP entity."""
|
||||
self._enabled_default = enabled_default
|
||||
self._entry_id = entry_id
|
||||
self._icon = icon
|
||||
self._name = name
|
||||
self._unsub_dispatcher = None
|
||||
self.coordinator = coordinator
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
"""Return the mdi icon of the entity."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return self.coordinator.last_update_success
|
||||
|
||||
@property
|
||||
def entity_registry_enabled_default(self) -> bool:
|
||||
"""Return if the entity should be enabled when first added to the entity registry."""
|
||||
return self._enabled_default
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Return the polling requirement of the entity."""
|
||||
return False
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Connect to dispatcher listening for entity data notifications."""
|
||||
self.coordinator.async_add_listener(self.async_write_ha_state)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Disconnect from update signal."""
|
||||
self.coordinator.async_remove_listener(self.async_write_ha_state)
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update an IPP entity."""
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@property
|
||||
def device_info(self) -> Dict[str, Any]:
|
||||
"""Return device information about this IPP device."""
|
||||
return {
|
||||
ATTR_IDENTIFIERS: {(DOMAIN, self.coordinator.data.info.uuid)},
|
||||
ATTR_NAME: self.coordinator.data.info.name,
|
||||
ATTR_MANUFACTURER: self.coordinator.data.info.manufacturer,
|
||||
ATTR_MODEL: self.coordinator.data.info.model,
|
||||
ATTR_SOFTWARE_VERSION: self.coordinator.data.info.version,
|
||||
}
|
144
homeassistant/components/ipp/config_flow.py
Normal file
144
homeassistant/components/ipp/config_flow.py
Normal file
|
@ -0,0 +1,144 @@
|
|||
"""Config flow to configure the IPP integration."""
|
||||
import logging
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from pyipp import IPP, IPPConnectionError, IPPConnectionUpgradeRequired
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
CONF_PORT,
|
||||
CONF_SSL,
|
||||
CONF_VERIFY_SSL,
|
||||
)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
||||
|
||||
from .const import CONF_BASE_PATH, CONF_UUID
|
||||
from .const import DOMAIN # pylint: disable=unused-import
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def validate_input(hass: HomeAssistantType, data: dict) -> Dict[str, Any]:
|
||||
"""Validate the user input allows us to connect.
|
||||
|
||||
Data has the keys from DATA_SCHEMA with values provided by the user.
|
||||
"""
|
||||
session = async_get_clientsession(hass)
|
||||
ipp = IPP(
|
||||
host=data[CONF_HOST],
|
||||
port=data[CONF_PORT],
|
||||
base_path=data[CONF_BASE_PATH],
|
||||
tls=data[CONF_SSL],
|
||||
verify_ssl=data[CONF_VERIFY_SSL],
|
||||
session=session,
|
||||
)
|
||||
|
||||
printer = await ipp.printer()
|
||||
|
||||
return {CONF_UUID: printer.info.uuid}
|
||||
|
||||
|
||||
class IPPFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle an IPP config flow."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL
|
||||
|
||||
def __init__(self):
|
||||
"""Set up the instance."""
|
||||
self.discovery_info = {}
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: Optional[ConfigType] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Handle a flow initiated by the user."""
|
||||
if user_input is None:
|
||||
return self._show_setup_form()
|
||||
|
||||
try:
|
||||
info = await validate_input(self.hass, user_input)
|
||||
except IPPConnectionUpgradeRequired:
|
||||
return self._show_setup_form({"base": "connection_upgrade"})
|
||||
except IPPConnectionError:
|
||||
return self._show_setup_form({"base": "connection_error"})
|
||||
user_input[CONF_UUID] = info[CONF_UUID]
|
||||
|
||||
await self.async_set_unique_id(user_input[CONF_UUID])
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: user_input[CONF_HOST]})
|
||||
|
||||
return self.async_create_entry(title=user_input[CONF_HOST], data=user_input)
|
||||
|
||||
async def async_step_zeroconf(self, discovery_info: ConfigType) -> Dict[str, Any]:
|
||||
"""Handle zeroconf discovery."""
|
||||
# Hostname is format: EPSON123456.local.
|
||||
host = discovery_info["hostname"].rstrip(".")
|
||||
port = discovery_info["port"]
|
||||
name, _ = host.rsplit(".")
|
||||
tls = discovery_info["type"] == "_ipps._tcp.local."
|
||||
|
||||
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
|
||||
self.context.update({"title_placeholders": {"name": name}})
|
||||
|
||||
self.discovery_info.update(
|
||||
{
|
||||
CONF_HOST: host,
|
||||
CONF_PORT: port,
|
||||
CONF_SSL: tls,
|
||||
CONF_VERIFY_SSL: False,
|
||||
CONF_BASE_PATH: "/"
|
||||
+ discovery_info["properties"].get("rp", "ipp/print"),
|
||||
CONF_NAME: name,
|
||||
CONF_UUID: discovery_info["properties"].get("UUID"),
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
info = await validate_input(self.hass, self.discovery_info)
|
||||
except IPPConnectionUpgradeRequired:
|
||||
return self.async_abort(reason="connection_upgrade")
|
||||
except IPPConnectionError:
|
||||
return self.async_abort(reason="connection_error")
|
||||
|
||||
self.discovery_info[CONF_UUID] = info[CONF_UUID]
|
||||
|
||||
await self.async_set_unique_id(self.discovery_info[CONF_UUID])
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={CONF_HOST: self.discovery_info[CONF_HOST]}
|
||||
)
|
||||
|
||||
return await self.async_step_zeroconf_confirm()
|
||||
|
||||
async def async_step_zeroconf_confirm(
|
||||
self, user_input: ConfigType = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Handle a confirmation flow initiated by zeroconf."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="zeroconf_confirm",
|
||||
description_placeholders={"name": self.discovery_info[CONF_NAME]},
|
||||
errors={},
|
||||
)
|
||||
|
||||
return self.async_create_entry(
|
||||
title=self.discovery_info[CONF_NAME], data=self.discovery_info,
|
||||
)
|
||||
|
||||
def _show_setup_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]:
|
||||
"""Show the setup form to the user."""
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Required(CONF_PORT, default=631): int,
|
||||
vol.Required(CONF_BASE_PATH, default="/ipp/print"): str,
|
||||
vol.Required(CONF_SSL, default=False): bool,
|
||||
vol.Required(CONF_VERIFY_SSL, default=False): bool,
|
||||
}
|
||||
),
|
||||
errors=errors or {},
|
||||
)
|
25
homeassistant/components/ipp/const.py
Normal file
25
homeassistant/components/ipp/const.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
"""Constants for the IPP integration."""
|
||||
|
||||
# Integration domain
|
||||
DOMAIN = "ipp"
|
||||
|
||||
# Attributes
|
||||
ATTR_COMMAND_SET = "command_set"
|
||||
ATTR_IDENTIFIERS = "identifiers"
|
||||
ATTR_INFO = "info"
|
||||
ATTR_LOCATION = "location"
|
||||
ATTR_MANUFACTURER = "manufacturer"
|
||||
ATTR_MARKER_TYPE = "marker_type"
|
||||
ATTR_MARKER_LOW_LEVEL = "marker_low_level"
|
||||
ATTR_MARKER_HIGH_LEVEL = "marker_high_level"
|
||||
ATTR_MODEL = "model"
|
||||
ATTR_SERIAL = "serial"
|
||||
ATTR_SOFTWARE_VERSION = "sw_version"
|
||||
ATTR_STATE_MESSAGE = "state_message"
|
||||
ATTR_STATE_REASON = "state_reason"
|
||||
ATTR_URI_SUPPORTED = "uri_supported"
|
||||
|
||||
# Config Keys
|
||||
CONF_BASE_PATH = "base_path"
|
||||
CONF_TLS = "tls"
|
||||
CONF_UUID = "uuid"
|
11
homeassistant/components/ipp/manifest.json
Normal file
11
homeassistant/components/ipp/manifest.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"domain": "ipp",
|
||||
"name": "Internet Printing Protocol (IPP)",
|
||||
"documentation": "https://www.home-assistant.io/integrations/ipp",
|
||||
"requirements": ["pyipp==0.8.1"],
|
||||
"dependencies": [],
|
||||
"codeowners": ["@ctalkington"],
|
||||
"config_flow": true,
|
||||
"quality_scale": "platinum",
|
||||
"zeroconf": ["_ipps._tcp.local.", "_ipp._tcp.local."]
|
||||
}
|
178
homeassistant/components/ipp/sensor.py
Normal file
178
homeassistant/components/ipp/sensor.py
Normal file
|
@ -0,0 +1,178 @@
|
|||
"""Support for IPP sensors."""
|
||||
from datetime import timedelta
|
||||
from typing import Any, Callable, Dict, List, Optional, Union
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import DEVICE_CLASS_TIMESTAMP, UNIT_PERCENTAGE
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from . import IPPDataUpdateCoordinator, IPPEntity
|
||||
from .const import (
|
||||
ATTR_COMMAND_SET,
|
||||
ATTR_INFO,
|
||||
ATTR_LOCATION,
|
||||
ATTR_MARKER_HIGH_LEVEL,
|
||||
ATTR_MARKER_LOW_LEVEL,
|
||||
ATTR_MARKER_TYPE,
|
||||
ATTR_SERIAL,
|
||||
ATTR_STATE_MESSAGE,
|
||||
ATTR_STATE_REASON,
|
||||
ATTR_URI_SUPPORTED,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistantType,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: Callable[[List[Entity], bool], None],
|
||||
) -> None:
|
||||
"""Set up IPP sensor based on a config entry."""
|
||||
coordinator: IPPDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
sensors = []
|
||||
|
||||
sensors.append(IPPPrinterSensor(entry.entry_id, coordinator))
|
||||
sensors.append(IPPUptimeSensor(entry.entry_id, coordinator))
|
||||
|
||||
for marker_index in range(len(coordinator.data.markers)):
|
||||
sensors.append(IPPMarkerSensor(entry.entry_id, coordinator, marker_index))
|
||||
|
||||
async_add_entities(sensors, True)
|
||||
|
||||
|
||||
class IPPSensor(IPPEntity):
|
||||
"""Defines an IPP sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
coordinator: IPPDataUpdateCoordinator,
|
||||
enabled_default: bool = True,
|
||||
entry_id: str,
|
||||
icon: str,
|
||||
key: str,
|
||||
name: str,
|
||||
unit_of_measurement: Optional[str] = None,
|
||||
) -> None:
|
||||
"""Initialize IPP sensor."""
|
||||
self._unit_of_measurement = unit_of_measurement
|
||||
self._key = key
|
||||
|
||||
super().__init__(
|
||||
entry_id=entry_id,
|
||||
coordinator=coordinator,
|
||||
name=name,
|
||||
icon=icon,
|
||||
enabled_default=enabled_default,
|
||||
)
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return the unique ID for this sensor."""
|
||||
return f"{self.coordinator.data.info.uuid}_{self._key}"
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self) -> str:
|
||||
"""Return the unit this state is expressed in."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
|
||||
class IPPMarkerSensor(IPPSensor):
|
||||
"""Defines an IPP marker sensor."""
|
||||
|
||||
def __init__(
|
||||
self, entry_id: str, coordinator: IPPDataUpdateCoordinator, marker_index: int
|
||||
) -> None:
|
||||
"""Initialize IPP marker sensor."""
|
||||
self.marker_index = marker_index
|
||||
|
||||
super().__init__(
|
||||
coordinator=coordinator,
|
||||
entry_id=entry_id,
|
||||
icon="mdi:water",
|
||||
key=f"marker_{marker_index}",
|
||||
name=f"{coordinator.data.info.name} {coordinator.data.markers[marker_index].name}",
|
||||
unit_of_measurement=UNIT_PERCENTAGE,
|
||||
)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self) -> Optional[Dict[str, Any]]:
|
||||
"""Return the state attributes of the entity."""
|
||||
return {
|
||||
ATTR_MARKER_HIGH_LEVEL: self.coordinator.data.markers[
|
||||
self.marker_index
|
||||
].high_level,
|
||||
ATTR_MARKER_LOW_LEVEL: self.coordinator.data.markers[
|
||||
self.marker_index
|
||||
].low_level,
|
||||
ATTR_MARKER_TYPE: self.coordinator.data.markers[
|
||||
self.marker_index
|
||||
].marker_type,
|
||||
}
|
||||
|
||||
@property
|
||||
def state(self) -> Union[None, str, int, float]:
|
||||
"""Return the state of the sensor."""
|
||||
return self.coordinator.data.markers[self.marker_index].level
|
||||
|
||||
|
||||
class IPPPrinterSensor(IPPSensor):
|
||||
"""Defines an IPP printer sensor."""
|
||||
|
||||
def __init__(self, entry_id: str, coordinator: IPPDataUpdateCoordinator) -> None:
|
||||
"""Initialize IPP printer sensor."""
|
||||
super().__init__(
|
||||
coordinator=coordinator,
|
||||
entry_id=entry_id,
|
||||
icon="mdi:printer",
|
||||
key="printer",
|
||||
name=coordinator.data.info.name,
|
||||
unit_of_measurement=None,
|
||||
)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self) -> Optional[Dict[str, Any]]:
|
||||
"""Return the state attributes of the entity."""
|
||||
return {
|
||||
ATTR_INFO: self.coordinator.data.info.printer_info,
|
||||
ATTR_SERIAL: self.coordinator.data.info.serial,
|
||||
ATTR_LOCATION: self.coordinator.data.info.location,
|
||||
ATTR_STATE_MESSAGE: self.coordinator.data.state.message,
|
||||
ATTR_STATE_REASON: self.coordinator.data.state.reasons,
|
||||
ATTR_COMMAND_SET: self.coordinator.data.info.command_set,
|
||||
ATTR_URI_SUPPORTED: self.coordinator.data.info.printer_uri_supported,
|
||||
}
|
||||
|
||||
@property
|
||||
def state(self) -> Union[None, str, int, float]:
|
||||
"""Return the state of the sensor."""
|
||||
return self.coordinator.data.state.printer_state
|
||||
|
||||
|
||||
class IPPUptimeSensor(IPPSensor):
|
||||
"""Defines a IPP uptime sensor."""
|
||||
|
||||
def __init__(self, entry_id: str, coordinator: IPPDataUpdateCoordinator) -> None:
|
||||
"""Initialize IPP uptime sensor."""
|
||||
super().__init__(
|
||||
coordinator=coordinator,
|
||||
enabled_default=False,
|
||||
entry_id=entry_id,
|
||||
icon="mdi:clock-outline",
|
||||
key="uptime",
|
||||
name=f"{coordinator.data.info.name} Uptime",
|
||||
)
|
||||
|
||||
@property
|
||||
def state(self) -> Union[None, str, int, float]:
|
||||
"""Return the state of the sensor."""
|
||||
uptime = utcnow() - timedelta(seconds=self.coordinator.data.info.uptime)
|
||||
return uptime.replace(microsecond=0).isoformat()
|
||||
|
||||
@property
|
||||
def device_class(self) -> Optional[str]:
|
||||
"""Return the class of this sensor."""
|
||||
return DEVICE_CLASS_TIMESTAMP
|
32
homeassistant/components/ipp/strings.json
Normal file
32
homeassistant/components/ipp/strings.json
Normal file
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"config": {
|
||||
"title": "Internet Printing Protocol (IPP)",
|
||||
"flow_title": "Printer: {name}",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Link your printer",
|
||||
"description": "Set up your printer via Internet Printing Protocol (IPP) to integrate with Home Assistant.",
|
||||
"data": {
|
||||
"host": "Host or IP address",
|
||||
"port": "Port",
|
||||
"base_path": "Relative path to the printer",
|
||||
"ssl": "Printer supports communication over SSL/TLS",
|
||||
"verify_ssl": "Printer uses a proper SSL certificate"
|
||||
}
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
"description": "Do you want to add the printer named `{name}` to Home Assistant?",
|
||||
"title": "Discovered printer"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"connection_error": "Failed to connect to printer.",
|
||||
"connection_upgrade": "Failed to connect to printer. Please try again with SSL/TLS option checked."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "This printer is already configured.",
|
||||
"connection_error": "Failed to connect to printer.",
|
||||
"connection_upgrade": "Failed to connect to printer due to connection upgrade being required."
|
||||
}
|
||||
}
|
||||
}
|
|
@ -54,6 +54,7 @@ FLOWS = [
|
|||
"ifttt",
|
||||
"ios",
|
||||
"ipma",
|
||||
"ipp",
|
||||
"iqvia",
|
||||
"izone",
|
||||
"konnected",
|
||||
|
|
|
@ -25,6 +25,12 @@ ZEROCONF = {
|
|||
"_hap._tcp.local.": [
|
||||
"homekit_controller"
|
||||
],
|
||||
"_ipp._tcp.local.": [
|
||||
"ipp"
|
||||
],
|
||||
"_ipps._tcp.local.": [
|
||||
"ipp"
|
||||
],
|
||||
"_printer._tcp.local.": [
|
||||
"brother"
|
||||
],
|
||||
|
|
|
@ -1335,6 +1335,9 @@ pyintesishome==1.7.1
|
|||
# homeassistant.components.ipma
|
||||
pyipma==2.0.5
|
||||
|
||||
# homeassistant.components.ipp
|
||||
pyipp==0.8.1
|
||||
|
||||
# homeassistant.components.iqvia
|
||||
pyiqvia==0.2.1
|
||||
|
||||
|
|
|
@ -518,6 +518,9 @@ pyicloud==0.9.6.1
|
|||
# homeassistant.components.ipma
|
||||
pyipma==2.0.5
|
||||
|
||||
# homeassistant.components.ipp
|
||||
pyipp==0.8.1
|
||||
|
||||
# homeassistant.components.iqvia
|
||||
pyiqvia==0.2.1
|
||||
|
||||
|
|
95
tests/components/ipp/__init__.py
Normal file
95
tests/components/ipp/__init__.py
Normal file
|
@ -0,0 +1,95 @@
|
|||
"""Tests for the IPP integration."""
|
||||
import os
|
||||
|
||||
from homeassistant.components.ipp.const import CONF_BASE_PATH, CONF_UUID, DOMAIN
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
CONF_PORT,
|
||||
CONF_SSL,
|
||||
CONF_TYPE,
|
||||
CONF_VERIFY_SSL,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
ATTR_HOSTNAME = "hostname"
|
||||
ATTR_PROPERTIES = "properties"
|
||||
|
||||
IPP_ZEROCONF_SERVICE_TYPE = "_ipp._tcp.local."
|
||||
IPPS_ZEROCONF_SERVICE_TYPE = "_ipps._tcp.local."
|
||||
|
||||
ZEROCONF_NAME = "EPSON123456"
|
||||
ZEROCONF_HOST = "1.2.3.4"
|
||||
ZEROCONF_HOSTNAME = "EPSON123456.local."
|
||||
ZEROCONF_PORT = 631
|
||||
|
||||
|
||||
MOCK_USER_INPUT = {
|
||||
CONF_HOST: "EPSON123456.local",
|
||||
CONF_PORT: 361,
|
||||
CONF_SSL: False,
|
||||
CONF_VERIFY_SSL: False,
|
||||
CONF_BASE_PATH: "/ipp/print",
|
||||
}
|
||||
|
||||
MOCK_ZEROCONF_IPP_SERVICE_INFO = {
|
||||
CONF_TYPE: IPP_ZEROCONF_SERVICE_TYPE,
|
||||
CONF_NAME: ZEROCONF_NAME,
|
||||
CONF_HOST: ZEROCONF_HOST,
|
||||
ATTR_HOSTNAME: ZEROCONF_HOSTNAME,
|
||||
CONF_PORT: ZEROCONF_PORT,
|
||||
ATTR_PROPERTIES: {"rp": "ipp/print"},
|
||||
}
|
||||
|
||||
MOCK_ZEROCONF_IPPS_SERVICE_INFO = {
|
||||
CONF_TYPE: IPPS_ZEROCONF_SERVICE_TYPE,
|
||||
CONF_NAME: ZEROCONF_NAME,
|
||||
CONF_HOST: ZEROCONF_HOST,
|
||||
ATTR_HOSTNAME: ZEROCONF_HOSTNAME,
|
||||
CONF_PORT: ZEROCONF_PORT,
|
||||
ATTR_PROPERTIES: {"rp": "ipp/print"},
|
||||
}
|
||||
|
||||
|
||||
def load_fixture_binary(filename):
|
||||
"""Load a binary fixture."""
|
||||
path = os.path.join(os.path.dirname(__file__), "..", "..", "fixtures", filename)
|
||||
with open(path, "rb") as fptr:
|
||||
return fptr.read()
|
||||
|
||||
|
||||
async def init_integration(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, skip_setup: bool = False,
|
||||
) -> MockConfigEntry:
|
||||
"""Set up the IPP integration in Home Assistant."""
|
||||
|
||||
fixture = "ipp/get-printer-attributes.bin"
|
||||
aioclient_mock.post(
|
||||
"http://EPSON123456.local:631/ipp/print",
|
||||
content=load_fixture_binary(fixture),
|
||||
headers={"Content-Type": "application/ipp"},
|
||||
)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id="cfe92100-67c4-11d4-a45f-f8d027761251",
|
||||
data={
|
||||
CONF_HOST: "EPSON123456.local",
|
||||
CONF_PORT: 631,
|
||||
CONF_SSL: False,
|
||||
CONF_VERIFY_SSL: True,
|
||||
CONF_BASE_PATH: "/ipp/print",
|
||||
CONF_UUID: "cfe92100-67c4-11d4-a45f-f8d027761251",
|
||||
},
|
||||
)
|
||||
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
if not skip_setup:
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return entry
|
306
tests/components/ipp/test_config_flow.py
Normal file
306
tests/components/ipp/test_config_flow.py
Normal file
|
@ -0,0 +1,306 @@
|
|||
"""Tests for the IPP config flow."""
|
||||
import aiohttp
|
||||
from pyipp import IPPConnectionUpgradeRequired
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.components.ipp import config_flow
|
||||
from homeassistant.components.ipp.const import CONF_BASE_PATH, CONF_UUID
|
||||
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SSL
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import (
|
||||
MOCK_USER_INPUT,
|
||||
MOCK_ZEROCONF_IPP_SERVICE_INFO,
|
||||
MOCK_ZEROCONF_IPPS_SERVICE_INFO,
|
||||
init_integration,
|
||||
load_fixture_binary,
|
||||
)
|
||||
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
|
||||
async def test_show_user_form(hass: HomeAssistant) -> None:
|
||||
"""Test that the user set up form is served."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN, context={"source": SOURCE_USER},
|
||||
)
|
||||
|
||||
assert result["step_id"] == "user"
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
||||
|
||||
async def test_show_zeroconf_confirm_form(hass: HomeAssistant) -> None:
|
||||
"""Test that the zeroconf confirmation form is served."""
|
||||
flow = config_flow.IPPFlowHandler()
|
||||
flow.hass = hass
|
||||
flow.context = {"source": SOURCE_ZEROCONF}
|
||||
flow.discovery_info = {CONF_NAME: "EPSON123456"}
|
||||
|
||||
result = await flow.async_step_zeroconf_confirm()
|
||||
|
||||
assert result["step_id"] == "zeroconf_confirm"
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["description_placeholders"] == {CONF_NAME: "EPSON123456"}
|
||||
|
||||
|
||||
async def test_show_zeroconf_form(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test that the zeroconf confirmation form is served."""
|
||||
aioclient_mock.post(
|
||||
"http://EPSON123456.local:631/ipp/print",
|
||||
content=load_fixture_binary("ipp/get-printer-attributes.bin"),
|
||||
headers={"Content-Type": "application/ipp"},
|
||||
)
|
||||
|
||||
flow = config_flow.IPPFlowHandler()
|
||||
flow.hass = hass
|
||||
flow.context = {"source": SOURCE_ZEROCONF}
|
||||
|
||||
discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy()
|
||||
result = await flow.async_step_zeroconf(discovery_info)
|
||||
|
||||
assert flow.discovery_info[CONF_HOST] == "EPSON123456.local"
|
||||
assert flow.discovery_info[CONF_NAME] == "EPSON123456"
|
||||
|
||||
assert result["step_id"] == "zeroconf_confirm"
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["description_placeholders"] == {CONF_NAME: "EPSON123456"}
|
||||
|
||||
|
||||
async def test_connection_error(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test we show user form on IPP connection error."""
|
||||
aioclient_mock.post(
|
||||
"http://EPSON123456.local:631/ipp/print", exc=aiohttp.ClientError
|
||||
)
|
||||
|
||||
user_input = MOCK_USER_INPUT.copy()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN, context={"source": SOURCE_USER}, data=user_input,
|
||||
)
|
||||
|
||||
assert result["step_id"] == "user"
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["errors"] == {"base": "connection_error"}
|
||||
|
||||
|
||||
async def test_zeroconf_connection_error(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test we abort zeroconf flow on IPP connection error."""
|
||||
aioclient_mock.post("http://EPSON123456.local/ipp/print", exc=aiohttp.ClientError)
|
||||
|
||||
discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info,
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "connection_error"
|
||||
|
||||
|
||||
async def test_zeroconf_confirm_connection_error(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test we abort zeroconf flow on IPP connection error."""
|
||||
aioclient_mock.post("http://EPSON123456.local/ipp/print", exc=aiohttp.ClientError)
|
||||
|
||||
discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN,
|
||||
context={
|
||||
"source": SOURCE_ZEROCONF,
|
||||
CONF_HOST: "EPSON123456.local",
|
||||
CONF_NAME: "EPSON123456",
|
||||
},
|
||||
data=discovery_info,
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "connection_error"
|
||||
|
||||
|
||||
async def test_user_connection_upgrade_required(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test we show the user form if connection upgrade required by server."""
|
||||
aioclient_mock.post(
|
||||
"http://EPSON123456.local:631/ipp/print", exc=IPPConnectionUpgradeRequired
|
||||
)
|
||||
|
||||
user_input = MOCK_USER_INPUT.copy()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN, context={"source": SOURCE_USER}, data=user_input,
|
||||
)
|
||||
|
||||
assert result["step_id"] == "user"
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["errors"] == {"base": "connection_upgrade"}
|
||||
|
||||
|
||||
async def test_zeroconf_connection_upgrade_required(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test we abort zeroconf flow on IPP connection error."""
|
||||
aioclient_mock.post(
|
||||
"http://EPSON123456.local/ipp/print", exc=IPPConnectionUpgradeRequired
|
||||
)
|
||||
|
||||
discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info,
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "connection_upgrade"
|
||||
|
||||
|
||||
async def test_user_device_exists_abort(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test we abort user flow if printer already configured."""
|
||||
await init_integration(hass, aioclient_mock)
|
||||
|
||||
user_input = MOCK_USER_INPUT.copy()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN, context={"source": SOURCE_USER}, data=user_input,
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_zeroconf_device_exists_abort(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test we abort zeroconf flow if printer already configured."""
|
||||
await init_integration(hass, aioclient_mock)
|
||||
|
||||
discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info,
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_zeroconf_with_uuid_device_exists_abort(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test we abort zeroconf flow if printer already configured."""
|
||||
await init_integration(hass, aioclient_mock)
|
||||
|
||||
discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy()
|
||||
discovery_info["properties"]["UUID"] = "cfe92100-67c4-11d4-a45f-f8d027761251"
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info,
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_full_user_flow_implementation(
|
||||
hass: HomeAssistant, aioclient_mock
|
||||
) -> None:
|
||||
"""Test the full manual user flow from start to finish."""
|
||||
aioclient_mock.post(
|
||||
"http://EPSON123456.local:631/ipp/print",
|
||||
content=load_fixture_binary("ipp/get-printer-attributes.bin"),
|
||||
headers={"Content-Type": "application/ipp"},
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN, context={"source": SOURCE_USER},
|
||||
)
|
||||
|
||||
assert result["step_id"] == "user"
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_HOST: "EPSON123456.local", CONF_BASE_PATH: "/ipp/print"},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == "EPSON123456.local"
|
||||
|
||||
assert result["data"]
|
||||
assert result["data"][CONF_HOST] == "EPSON123456.local"
|
||||
assert result["data"][CONF_UUID] == "cfe92100-67c4-11d4-a45f-f8d027761251"
|
||||
|
||||
|
||||
async def test_full_zeroconf_flow_implementation(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test the full manual user flow from start to finish."""
|
||||
aioclient_mock.post(
|
||||
"http://EPSON123456.local:631/ipp/print",
|
||||
content=load_fixture_binary("ipp/get-printer-attributes.bin"),
|
||||
headers={"Content-Type": "application/ipp"},
|
||||
)
|
||||
|
||||
flow = config_flow.IPPFlowHandler()
|
||||
flow.hass = hass
|
||||
flow.context = {"source": SOURCE_ZEROCONF}
|
||||
|
||||
discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy()
|
||||
result = await flow.async_step_zeroconf(discovery_info)
|
||||
|
||||
assert flow.discovery_info
|
||||
assert flow.discovery_info[CONF_HOST] == "EPSON123456.local"
|
||||
assert flow.discovery_info[CONF_NAME] == "EPSON123456"
|
||||
|
||||
assert result["step_id"] == "zeroconf_confirm"
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
||||
result = await flow.async_step_zeroconf_confirm(
|
||||
user_input={CONF_HOST: "EPSON123456.local"}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == "EPSON123456"
|
||||
|
||||
assert result["data"]
|
||||
assert result["data"][CONF_HOST] == "EPSON123456.local"
|
||||
assert result["data"][CONF_UUID] == "cfe92100-67c4-11d4-a45f-f8d027761251"
|
||||
assert not result["data"][CONF_SSL]
|
||||
|
||||
|
||||
async def test_full_zeroconf_tls_flow_implementation(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test the full manual user flow from start to finish."""
|
||||
aioclient_mock.post(
|
||||
"https://EPSON123456.local:631/ipp/print",
|
||||
content=load_fixture_binary("ipp/get-printer-attributes.bin"),
|
||||
headers={"Content-Type": "application/ipp"},
|
||||
)
|
||||
|
||||
flow = config_flow.IPPFlowHandler()
|
||||
flow.hass = hass
|
||||
flow.context = {"source": SOURCE_ZEROCONF}
|
||||
|
||||
discovery_info = MOCK_ZEROCONF_IPPS_SERVICE_INFO.copy()
|
||||
result = await flow.async_step_zeroconf(discovery_info)
|
||||
|
||||
assert result["step_id"] == "zeroconf_confirm"
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["description_placeholders"] == {CONF_NAME: "EPSON123456"}
|
||||
|
||||
result = await flow.async_step_zeroconf_confirm(
|
||||
user_input={CONF_HOST: "EPSON123456.local"}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == "EPSON123456"
|
||||
|
||||
assert result["data"]
|
||||
assert result["data"][CONF_HOST] == "EPSON123456.local"
|
||||
assert result["data"][CONF_NAME] == "EPSON123456"
|
||||
assert result["data"][CONF_UUID] == "cfe92100-67c4-11d4-a45f-f8d027761251"
|
||||
assert result["data"][CONF_SSL]
|
42
tests/components/ipp/test_init.py
Normal file
42
tests/components/ipp/test_init.py
Normal file
|
@ -0,0 +1,42 @@
|
|||
"""Tests for the IPP integration."""
|
||||
import aiohttp
|
||||
|
||||
from homeassistant.components.ipp.const import DOMAIN
|
||||
from homeassistant.config_entries import (
|
||||
ENTRY_STATE_LOADED,
|
||||
ENTRY_STATE_NOT_LOADED,
|
||||
ENTRY_STATE_SETUP_RETRY,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.components.ipp import init_integration
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
|
||||
async def test_config_entry_not_ready(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test the IPP configuration entry not ready."""
|
||||
aioclient_mock.post(
|
||||
"http://EPSON123456.local:631/ipp/print", exc=aiohttp.ClientError
|
||||
)
|
||||
|
||||
entry = await init_integration(hass, aioclient_mock)
|
||||
assert entry.state == ENTRY_STATE_SETUP_RETRY
|
||||
|
||||
|
||||
async def test_unload_config_entry(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test the IPP configuration entry unloading."""
|
||||
entry = await init_integration(hass, aioclient_mock)
|
||||
|
||||
assert hass.data[DOMAIN]
|
||||
assert entry.entry_id in hass.data[DOMAIN]
|
||||
assert entry.state == ENTRY_STATE_LOADED
|
||||
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entry.entry_id not in hass.data[DOMAIN]
|
||||
assert entry.state == ENTRY_STATE_NOT_LOADED
|
96
tests/components/ipp/test_sensor.py
Normal file
96
tests/components/ipp/test_sensor.py
Normal file
|
@ -0,0 +1,96 @@
|
|||
"""Tests for the IPP sensor platform."""
|
||||
from datetime import datetime
|
||||
|
||||
from asynctest import patch
|
||||
|
||||
from homeassistant.components.ipp.const import DOMAIN
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.const import ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, UNIT_PERCENTAGE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from tests.components.ipp import init_integration
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
|
||||
async def test_sensors(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test the creation and values of the IPP sensors."""
|
||||
entry = await init_integration(hass, aioclient_mock, skip_setup=True)
|
||||
registry = await hass.helpers.entity_registry.async_get_registry()
|
||||
|
||||
# Pre-create registry entries for disabled by default sensors
|
||||
registry.async_get_or_create(
|
||||
SENSOR_DOMAIN,
|
||||
DOMAIN,
|
||||
"cfe92100-67c4-11d4-a45f-f8d027761251_uptime",
|
||||
suggested_object_id="epson_xp_6000_series_uptime",
|
||||
disabled_by=None,
|
||||
)
|
||||
|
||||
test_time = datetime(2019, 11, 11, 9, 10, 32, tzinfo=dt_util.UTC)
|
||||
with patch("homeassistant.components.ipp.sensor.utcnow", return_value=test_time):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("sensor.epson_xp_6000_series")
|
||||
assert state
|
||||
assert state.attributes.get(ATTR_ICON) == "mdi:printer"
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None
|
||||
|
||||
state = hass.states.get("sensor.epson_xp_6000_series_black_ink")
|
||||
assert state
|
||||
assert state.attributes.get(ATTR_ICON) == "mdi:water"
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is UNIT_PERCENTAGE
|
||||
assert state.state == "58"
|
||||
|
||||
state = hass.states.get("sensor.epson_xp_6000_series_photo_black_ink")
|
||||
assert state
|
||||
assert state.attributes.get(ATTR_ICON) == "mdi:water"
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is UNIT_PERCENTAGE
|
||||
assert state.state == "98"
|
||||
|
||||
state = hass.states.get("sensor.epson_xp_6000_series_cyan_ink")
|
||||
assert state
|
||||
assert state.attributes.get(ATTR_ICON) == "mdi:water"
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is UNIT_PERCENTAGE
|
||||
assert state.state == "91"
|
||||
|
||||
state = hass.states.get("sensor.epson_xp_6000_series_yellow_ink")
|
||||
assert state
|
||||
assert state.attributes.get(ATTR_ICON) == "mdi:water"
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is UNIT_PERCENTAGE
|
||||
assert state.state == "95"
|
||||
|
||||
state = hass.states.get("sensor.epson_xp_6000_series_magenta_ink")
|
||||
assert state
|
||||
assert state.attributes.get(ATTR_ICON) == "mdi:water"
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is UNIT_PERCENTAGE
|
||||
assert state.state == "73"
|
||||
|
||||
state = hass.states.get("sensor.epson_xp_6000_series_uptime")
|
||||
assert state
|
||||
assert state.attributes.get(ATTR_ICON) == "mdi:clock-outline"
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None
|
||||
assert state.state == "2019-10-26T15:37:00+00:00"
|
||||
|
||||
entry = registry.async_get("sensor.epson_xp_6000_series_uptime")
|
||||
assert entry
|
||||
assert entry.unique_id == "cfe92100-67c4-11d4-a45f-f8d027761251_uptime"
|
||||
|
||||
|
||||
async def test_disabled_by_default_sensors(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test the disabled by default IPP sensors."""
|
||||
await init_integration(hass, aioclient_mock)
|
||||
registry = await hass.helpers.entity_registry.async_get_registry()
|
||||
|
||||
state = hass.states.get("sensor.epson_xp_6000_series_uptime")
|
||||
assert state is None
|
||||
|
||||
entry = registry.async_get("sensor.epson_xp_6000_series_uptime")
|
||||
assert entry
|
||||
assert entry.disabled
|
||||
assert entry.disabled_by == "integration"
|
BIN
tests/fixtures/ipp/get-printer-attributes.bin
vendored
Normal file
BIN
tests/fixtures/ipp/get-printer-attributes.bin
vendored
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue