Remove 1-Wire SysBus (ADR-0019) (#71232)

This commit is contained in:
epenet 2022-05-09 13:16:23 +02:00 committed by GitHub
parent 30fdfc454f
commit 08856cfab0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 142 additions and 909 deletions

View file

@ -3,7 +3,6 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
import os import os
from typing import TYPE_CHECKING
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass, BinarySensorDeviceClass,
@ -11,21 +10,18 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntityDescription, BinarySensorEntityDescription,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_TYPE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import ( from .const import (
CONF_TYPE_OWSERVER,
DEVICE_KEYS_0_3, DEVICE_KEYS_0_3,
DEVICE_KEYS_0_7, DEVICE_KEYS_0_7,
DEVICE_KEYS_A_B, DEVICE_KEYS_A_B,
DOMAIN, DOMAIN,
READ_MODE_BOOL, READ_MODE_BOOL,
) )
from .model import OWServerDeviceDescription from .onewire_entities import OneWireEntity, OneWireEntityDescription
from .onewire_entities import OneWireEntityDescription, OneWireProxyEntity
from .onewirehub import OneWireHub from .onewirehub import OneWireHub
@ -98,23 +94,19 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up 1-Wire platform.""" """Set up 1-Wire platform."""
# Only OWServer implementation works with binary sensors onewirehub = hass.data[DOMAIN][config_entry.entry_id]
if config_entry.data[CONF_TYPE] == CONF_TYPE_OWSERVER:
onewirehub = hass.data[DOMAIN][config_entry.entry_id]
entities = await hass.async_add_executor_job(get_entities, onewirehub) entities = await hass.async_add_executor_job(get_entities, onewirehub)
async_add_entities(entities, True) async_add_entities(entities, True)
def get_entities(onewirehub: OneWireHub) -> list[BinarySensorEntity]: def get_entities(onewirehub: OneWireHub) -> list[OneWireBinarySensor]:
"""Get a list of entities.""" """Get a list of entities."""
if not onewirehub.devices: if not onewirehub.devices:
return [] return []
entities: list[BinarySensorEntity] = [] entities: list[OneWireBinarySensor] = []
for device in onewirehub.devices: for device in onewirehub.devices:
if TYPE_CHECKING:
assert isinstance(device, OWServerDeviceDescription)
family = device.family family = device.family
device_id = device.id device_id = device.id
device_type = device.type device_type = device.type
@ -130,7 +122,7 @@ def get_entities(onewirehub: OneWireHub) -> list[BinarySensorEntity]:
device_file = os.path.join(os.path.split(device.path)[0], description.key) device_file = os.path.join(os.path.split(device.path)[0], description.key)
name = f"{device_id} {description.name}" name = f"{device_id} {description.name}"
entities.append( entities.append(
OneWireProxyBinarySensor( OneWireBinarySensor(
description=description, description=description,
device_id=device_id, device_id=device_id,
device_file=device_file, device_file=device_file,
@ -143,7 +135,7 @@ def get_entities(onewirehub: OneWireHub) -> list[BinarySensorEntity]:
return entities return entities
class OneWireProxyBinarySensor(OneWireProxyEntity, BinarySensorEntity): class OneWireBinarySensor(OneWireEntity, BinarySensorEntity):
"""Implementation of a 1-Wire binary sensor.""" """Implementation of a 1-Wire binary sensor."""
entity_description: OneWireBinarySensorEntityDescription entity_description: OneWireBinarySensorEntityDescription

View file

@ -6,19 +6,15 @@ from typing import Any
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.device_registry import DeviceRegistry from homeassistant.helpers.device_registry import DeviceRegistry
from .const import ( from .const import (
CONF_MOUNT_DIR, DEFAULT_HOST,
CONF_TYPE_OWSERVER, DEFAULT_PORT,
CONF_TYPE_SYSBUS,
DEFAULT_OWSERVER_HOST,
DEFAULT_OWSERVER_PORT,
DEFAULT_SYSBUS_MOUNT_DIR,
DEVICE_SUPPORT_OPTIONS, DEVICE_SUPPORT_OPTIONS,
DOMAIN, DOMAIN,
INPUT_ENTRY_CLEAR_OPTIONS, INPUT_ENTRY_CLEAR_OPTIONS,
@ -27,31 +23,21 @@ from .const import (
OPTION_ENTRY_SENSOR_PRECISION, OPTION_ENTRY_SENSOR_PRECISION,
PRECISION_MAPPING_FAMILY_28, PRECISION_MAPPING_FAMILY_28,
) )
from .model import OWServerDeviceDescription from .model import OWDeviceDescription
from .onewirehub import CannotConnect, InvalidPath, OneWireHub from .onewirehub import CannotConnect, OneWireHub
DATA_SCHEMA_USER = vol.Schema( DATA_SCHEMA = vol.Schema(
{vol.Required(CONF_TYPE): vol.In([CONF_TYPE_OWSERVER, CONF_TYPE_SYSBUS])}
)
DATA_SCHEMA_OWSERVER = vol.Schema(
{ {
vol.Required(CONF_HOST, default=DEFAULT_OWSERVER_HOST): str, vol.Required(CONF_HOST, default=DEFAULT_HOST): str,
vol.Required(CONF_PORT, default=DEFAULT_OWSERVER_PORT): int, vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
}
)
DATA_SCHEMA_MOUNTDIR = vol.Schema(
{
vol.Required(CONF_MOUNT_DIR, default=DEFAULT_SYSBUS_MOUNT_DIR): str,
} }
) )
async def validate_input_owserver( async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]:
hass: HomeAssistant, data: dict[str, Any]
) -> dict[str, str]:
"""Validate the user input allows us to connect. """Validate the user input allows us to connect.
Data has the keys from DATA_SCHEMA_OWSERVER with values provided by the user. Data has the keys from DATA_SCHEMA with values provided by the user.
""" """
hub = OneWireHub(hass) hub = OneWireHub(hass)
@ -65,24 +51,6 @@ async def validate_input_owserver(
return {"title": host} return {"title": host}
async def validate_input_mount_dir(
hass: HomeAssistant, data: dict[str, Any]
) -> dict[str, str]:
"""Validate the user input allows us to connect.
Data has the keys from DATA_SCHEMA_MOUNTDIR with values provided by the user.
"""
hub = OneWireHub(hass)
mount_dir = data[CONF_MOUNT_DIR]
# Raises InvalidDir exception on failure
await hub.check_mount_dir(mount_dir)
# Return info that you want to store in the config entry.
return {"title": mount_dir}
class OneWireFlowHandler(ConfigFlow, domain=DOMAIN): class OneWireFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle 1-Wire config flow.""" """Handle 1-Wire config flow."""
@ -100,29 +68,10 @@ class OneWireFlowHandler(ConfigFlow, domain=DOMAIN):
Let user manually input configuration. Let user manually input configuration.
""" """
errors: dict[str, str] = {} errors: dict[str, str] = {}
if user_input is not None:
self.onewire_config.update(user_input)
if CONF_TYPE_OWSERVER == user_input[CONF_TYPE]:
return await self.async_step_owserver()
if CONF_TYPE_SYSBUS == user_input[CONF_TYPE]:
return await self.async_step_mount_dir()
return self.async_show_form(
step_id="user",
data_schema=DATA_SCHEMA_USER,
errors=errors,
)
async def async_step_owserver(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle OWServer configuration."""
errors = {}
if user_input: if user_input:
# Prevent duplicate entries # Prevent duplicate entries
self._async_abort_entries_match( self._async_abort_entries_match(
{ {
CONF_TYPE: CONF_TYPE_OWSERVER,
CONF_HOST: user_input[CONF_HOST], CONF_HOST: user_input[CONF_HOST],
CONF_PORT: user_input[CONF_PORT], CONF_PORT: user_input[CONF_PORT],
} }
@ -131,7 +80,7 @@ class OneWireFlowHandler(ConfigFlow, domain=DOMAIN):
self.onewire_config.update(user_input) self.onewire_config.update(user_input)
try: try:
info = await validate_input_owserver(self.hass, user_input) info = await validate_input(self.hass, user_input)
except CannotConnect: except CannotConnect:
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
else: else:
@ -140,37 +89,8 @@ class OneWireFlowHandler(ConfigFlow, domain=DOMAIN):
) )
return self.async_show_form( return self.async_show_form(
step_id="owserver", step_id="user",
data_schema=DATA_SCHEMA_OWSERVER, data_schema=DATA_SCHEMA,
errors=errors,
)
async def async_step_mount_dir(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle SysBus configuration."""
errors = {}
if user_input:
# Prevent duplicate entries
await self.async_set_unique_id(
f"{CONF_TYPE_SYSBUS}:{user_input[CONF_MOUNT_DIR]}"
)
self._abort_if_unique_id_configured()
self.onewire_config.update(user_input)
try:
info = await validate_input_mount_dir(self.hass, user_input)
except InvalidPath:
errors["base"] = "invalid_path"
else:
return self.async_create_entry(
title=info["title"], data=self.onewire_config
)
return self.async_show_form(
step_id="mount_dir",
data_schema=DATA_SCHEMA_MOUNTDIR,
errors=errors, errors=errors,
) )
@ -188,8 +108,8 @@ class OnewireOptionsFlowHandler(OptionsFlow):
"""Initialize OneWire Network options flow.""" """Initialize OneWire Network options flow."""
self.entry_id = config_entry.entry_id self.entry_id = config_entry.entry_id
self.options = dict(config_entry.options) self.options = dict(config_entry.options)
self.configurable_devices: dict[str, OWServerDeviceDescription] = {} self.configurable_devices: dict[str, OWDeviceDescription] = {}
self.devices_to_configure: dict[str, OWServerDeviceDescription] = {} self.devices_to_configure: dict[str, OWDeviceDescription] = {}
self.current_device: str = "" self.current_device: str = ""
async def async_step_init( async def async_step_init(
@ -197,12 +117,7 @@ class OnewireOptionsFlowHandler(OptionsFlow):
) -> FlowResult: ) -> FlowResult:
"""Manage the options.""" """Manage the options."""
controller: OneWireHub = self.hass.data[DOMAIN][self.entry_id] controller: OneWireHub = self.hass.data[DOMAIN][self.entry_id]
if controller.type == CONF_TYPE_SYSBUS: all_devices: list[OWDeviceDescription] = controller.devices # type: ignore[assignment]
return self.async_abort(
reason="SysBus setup does not have any config options."
)
all_devices: list[OWServerDeviceDescription] = controller.devices # type: ignore[assignment]
if not all_devices: if not all_devices:
return self.async_abort(reason="No configurable devices found.") return self.async_abort(reason="No configurable devices found.")

View file

@ -3,14 +3,8 @@ from __future__ import annotations
from homeassistant.const import Platform from homeassistant.const import Platform
CONF_MOUNT_DIR = "mount_dir" DEFAULT_HOST = "localhost"
DEFAULT_PORT = 4304
CONF_TYPE_OWSERVER = "OWServer"
CONF_TYPE_SYSBUS = "SysBus"
DEFAULT_OWSERVER_HOST = "localhost"
DEFAULT_OWSERVER_PORT = 4304
DEFAULT_SYSBUS_MOUNT_DIR = "/sys/bus/w1/devices/"
DOMAIN = "onewire" DOMAIN = "onewire"
@ -18,7 +12,7 @@ DEVICE_KEYS_0_3 = range(4)
DEVICE_KEYS_0_7 = range(8) DEVICE_KEYS_0_7 = range(8)
DEVICE_KEYS_A_B = ("A", "B") DEVICE_KEYS_A_B = ("A", "B")
DEVICE_SUPPORT_OWSERVER = { DEVICE_SUPPORT = {
"05": (), "05": (),
"10": (), "10": (),
"12": (), "12": (),
@ -35,7 +29,6 @@ DEVICE_SUPPORT_OWSERVER = {
"7E": ("EDS0066", "EDS0068"), "7E": ("EDS0066", "EDS0068"),
"EF": ("HB_HUB", "HB_MOISTURE_METER", "HobbyBoards_EF"), "EF": ("HB_HUB", "HB_MOISTURE_METER", "HobbyBoards_EF"),
} }
DEVICE_SUPPORT_SYSBUS = ["10", "22", "28", "3B", "42"]
DEVICE_SUPPORT_OPTIONS = ["28"] DEVICE_SUPPORT_OPTIONS = ["28"]

View file

@ -3,8 +3,8 @@
"name": "1-Wire", "name": "1-Wire",
"documentation": "https://www.home-assistant.io/integrations/onewire", "documentation": "https://www.home-assistant.io/integrations/onewire",
"config_flow": true, "config_flow": true,
"requirements": ["pyownet==0.10.0.post1", "pi1wire==0.1.0"], "requirements": ["pyownet==0.10.0.post1"],
"codeowners": ["@garbled1", "@epenet"], "codeowners": ["@garbled1", "@epenet"],
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["pi1wire", "pyownet"] "loggers": ["pyownet"]
} }

View file

@ -3,29 +3,15 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from pi1wire import OneWireInterface
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
@dataclass @dataclass
class OWDeviceDescription: class OWDeviceDescription:
"""OWDeviceDescription device description class.""" """1-Wire device description class."""
device_info: DeviceInfo device_info: DeviceInfo
@dataclass
class OWDirectDeviceDescription(OWDeviceDescription):
"""SysBus device description class."""
interface: OneWireInterface
@dataclass
class OWServerDeviceDescription(OWDeviceDescription):
"""OWServer device description class."""
family: str family: str
id: str id: str
path: str path: str

View file

@ -23,7 +23,7 @@ class OneWireEntityDescription(EntityDescription):
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class OneWireBaseEntity(Entity): class OneWireEntity(Entity):
"""Implementation of a 1-Wire entity.""" """Implementation of a 1-Wire entity."""
entity_description: OneWireEntityDescription entity_description: OneWireEntityDescription
@ -35,6 +35,7 @@ class OneWireBaseEntity(Entity):
device_info: DeviceInfo, device_info: DeviceInfo,
device_file: str, device_file: str,
name: str, name: str,
owproxy: protocol._Proxy,
) -> None: ) -> None:
"""Initialize the entity.""" """Initialize the entity."""
self.entity_description = description self.entity_description = description
@ -44,6 +45,7 @@ class OneWireBaseEntity(Entity):
self._device_file = device_file self._device_file = device_file
self._state: StateType = None self._state: StateType = None
self._value_raw: float | None = None self._value_raw: float | None = None
self._owproxy = owproxy
@property @property
def extra_state_attributes(self) -> dict[str, Any] | None: def extra_state_attributes(self) -> dict[str, Any] | None:
@ -53,44 +55,21 @@ class OneWireBaseEntity(Entity):
"raw_value": self._value_raw, "raw_value": self._value_raw,
} }
def _read_value(self) -> str:
class OneWireProxyEntity(OneWireBaseEntity): """Read a value from the server."""
"""Implementation of a 1-Wire entity connected through owserver."""
def __init__(
self,
description: OneWireEntityDescription,
device_id: str,
device_info: DeviceInfo,
device_file: str,
name: str,
owproxy: protocol._Proxy,
) -> None:
"""Initialize the sensor."""
super().__init__(
description=description,
device_id=device_id,
device_info=device_info,
device_file=device_file,
name=name,
)
self._owproxy = owproxy
def _read_value_ownet(self) -> str:
"""Read a value from the owserver."""
read_bytes: bytes = self._owproxy.read(self._device_file) read_bytes: bytes = self._owproxy.read(self._device_file)
return read_bytes.decode().lstrip() return read_bytes.decode().lstrip()
def _write_value_ownet(self, value: bytes) -> None: def _write_value(self, value: bytes) -> None:
"""Write a value to the owserver.""" """Write a value to the server."""
self._owproxy.write(self._device_file, value) self._owproxy.write(self._device_file, value)
def update(self) -> None: def update(self) -> None:
"""Get the latest data from the device.""" """Get the latest data from the device."""
try: try:
self._value_raw = float(self._read_value_ownet()) self._value_raw = float(self._read_value())
except protocol.Error as exc: except protocol.Error as exc:
_LOGGER.error("Owserver failure in read(), got: %s", exc) _LOGGER.error("Failure to read server value, got: %s", exc)
self._state = None self._state = None
else: else:
if self.entity_description.read_mode == READ_MODE_INT: if self.entity_description.read_mode == READ_MODE_INT:

View file

@ -5,7 +5,6 @@ import logging
import os import os
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from pi1wire import Pi1Wire
from pyownet import protocol from pyownet import protocol
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -17,7 +16,6 @@ from homeassistant.const import (
ATTR_VIA_DEVICE, ATTR_VIA_DEVICE,
CONF_HOST, CONF_HOST,
CONF_PORT, CONF_PORT,
CONF_TYPE,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
@ -25,21 +23,13 @@ from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from .const import ( from .const import (
CONF_MOUNT_DIR, DEVICE_SUPPORT,
CONF_TYPE_OWSERVER,
CONF_TYPE_SYSBUS,
DEVICE_SUPPORT_OWSERVER,
DEVICE_SUPPORT_SYSBUS,
DOMAIN, DOMAIN,
MANUFACTURER_EDS, MANUFACTURER_EDS,
MANUFACTURER_HOBBYBOARDS, MANUFACTURER_HOBBYBOARDS,
MANUFACTURER_MAXIM, MANUFACTURER_MAXIM,
) )
from .model import ( from .model import OWDeviceDescription
OWDeviceDescription,
OWDirectDeviceDescription,
OWServerDeviceDescription,
)
DEVICE_COUPLERS = { DEVICE_COUPLERS = {
# Family : [branches] # Family : [branches]
@ -54,26 +44,24 @@ DEVICE_MANUFACTURER = {
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def _is_known_owserver_device(device_family: str, device_type: str) -> bool: def _is_known_device(device_family: str, device_type: str) -> bool:
"""Check if device family/type is known to the library.""" """Check if device family/type is known to the library."""
if device_family in ("7E", "EF"): # EDS or HobbyBoard if device_family in ("7E", "EF"): # EDS or HobbyBoard
return device_type in DEVICE_SUPPORT_OWSERVER[device_family] return device_type in DEVICE_SUPPORT[device_family]
return device_family in DEVICE_SUPPORT_OWSERVER return device_family in DEVICE_SUPPORT
class OneWireHub: class OneWireHub:
"""Hub to communicate with SysBus or OWServer.""" """Hub to communicate with server."""
def __init__(self, hass: HomeAssistant) -> None: def __init__(self, hass: HomeAssistant) -> None:
"""Initialize.""" """Initialize."""
self.hass = hass self.hass = hass
self.type: str | None = None
self.pi1proxy: Pi1Wire | None = None
self.owproxy: protocol._Proxy | None = None self.owproxy: protocol._Proxy | None = None
self.devices: list[OWDeviceDescription] | None = None self.devices: list[OWDeviceDescription] | None = None
async def connect(self, host: str, port: int) -> None: async def connect(self, host: str, port: int) -> None:
"""Connect to the owserver host.""" """Connect to the server."""
try: try:
self.owproxy = await self.hass.async_add_executor_job( self.owproxy = await self.hass.async_add_executor_job(
protocol.proxy, host, port protocol.proxy, host, port
@ -81,32 +69,12 @@ class OneWireHub:
except protocol.ConnError as exc: except protocol.ConnError as exc:
raise CannotConnect from exc raise CannotConnect from exc
async def check_mount_dir(self, mount_dir: str) -> None:
"""Test that the mount_dir is a valid path."""
if not await self.hass.async_add_executor_job(os.path.isdir, mount_dir):
raise InvalidPath
self.pi1proxy = Pi1Wire(mount_dir)
async def initialize(self, config_entry: ConfigEntry) -> None: async def initialize(self, config_entry: ConfigEntry) -> None:
"""Initialize a config entry.""" """Initialize a config entry."""
self.type = config_entry.data[CONF_TYPE] host = config_entry.data[CONF_HOST]
if self.type == CONF_TYPE_SYSBUS: port = config_entry.data[CONF_PORT]
mount_dir = config_entry.data[CONF_MOUNT_DIR] _LOGGER.debug("Initializing connection to %s:%s", host, port)
_LOGGER.debug("Initializing using SysBus %s", mount_dir) await self.connect(host, port)
_LOGGER.warning(
"Using the 1-Wire integration via SysBus is deprecated and will be removed "
"in Home Assistant Core 2022.6; this integration is being adjusted to comply "
"with Architectural Decision Record 0019, more information can be found here: "
"https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md "
"Access via OWServer is still supported"
)
await self.check_mount_dir(mount_dir)
elif self.type == CONF_TYPE_OWSERVER:
host = config_entry.data[CONF_HOST]
port = config_entry.data[CONF_PORT]
_LOGGER.debug("Initializing using OWServer %s:%s", host, port)
await self.connect(host, port)
await self.discover_devices() await self.discover_devices()
if TYPE_CHECKING: if TYPE_CHECKING:
assert self.devices assert self.devices
@ -126,63 +94,22 @@ class OneWireHub:
async def discover_devices(self) -> None: async def discover_devices(self) -> None:
"""Discover all devices.""" """Discover all devices."""
if self.devices is None: if self.devices is None:
if self.type == CONF_TYPE_SYSBUS: self.devices = await self.hass.async_add_executor_job(
self.devices = await self.hass.async_add_executor_job( self._discover_devices
self._discover_devices_sysbus
)
if self.type == CONF_TYPE_OWSERVER:
self.devices = await self.hass.async_add_executor_job(
self._discover_devices_owserver
)
def _discover_devices_sysbus(self) -> list[OWDeviceDescription]:
"""Discover all sysbus devices."""
devices: list[OWDeviceDescription] = []
assert self.pi1proxy
all_sensors = self.pi1proxy.find_all_sensors()
if not all_sensors:
_LOGGER.error(
"No onewire sensor found. Check if dtoverlay=w1-gpio "
"is in your /boot/config.txt. "
"Check the mount_dir parameter if it's defined"
) )
for interface in all_sensors:
device_family = interface.mac_address[:2]
device_id = f"{device_family}-{interface.mac_address[2:]}"
if device_family not in DEVICE_SUPPORT_SYSBUS:
_LOGGER.warning(
"Ignoring unknown device family (%s) found for device %s",
device_family,
device_id,
)
continue
device_info: DeviceInfo = {
ATTR_IDENTIFIERS: {(DOMAIN, device_id)},
ATTR_MANUFACTURER: DEVICE_MANUFACTURER.get(
device_family, MANUFACTURER_MAXIM
),
ATTR_MODEL: device_family,
ATTR_NAME: device_id,
}
device = OWDirectDeviceDescription(
device_info=device_info,
interface=interface,
)
devices.append(device)
return devices
def _discover_devices_owserver( def _discover_devices(
self, path: str = "/", parent_id: str | None = None self, path: str = "/", parent_id: str | None = None
) -> list[OWDeviceDescription]: ) -> list[OWDeviceDescription]:
"""Discover all owserver devices.""" """Discover all server devices."""
devices: list[OWDeviceDescription] = [] devices: list[OWDeviceDescription] = []
assert self.owproxy assert self.owproxy
for device_path in self.owproxy.dir(path): for device_path in self.owproxy.dir(path):
device_id = os.path.split(os.path.split(device_path)[0])[1] device_id = os.path.split(os.path.split(device_path)[0])[1]
device_family = self.owproxy.read(f"{device_path}family").decode() device_family = self.owproxy.read(f"{device_path}family").decode()
_LOGGER.debug("read `%sfamily`: %s", device_path, device_family) _LOGGER.debug("read `%sfamily`: %s", device_path, device_family)
device_type = self._get_device_type_owserver(device_path) device_type = self._get_device_type(device_path)
if not _is_known_owserver_device(device_family, device_type): if not _is_known_device(device_family, device_type):
_LOGGER.warning( _LOGGER.warning(
"Ignoring unknown device family/type (%s/%s) found for device %s", "Ignoring unknown device family/type (%s/%s) found for device %s",
device_family, device_family,
@ -200,7 +127,7 @@ class OneWireHub:
} }
if parent_id: if parent_id:
device_info[ATTR_VIA_DEVICE] = (DOMAIN, parent_id) device_info[ATTR_VIA_DEVICE] = (DOMAIN, parent_id)
device = OWServerDeviceDescription( device = OWDeviceDescription(
device_info=device_info, device_info=device_info,
id=device_id, id=device_id,
family=device_family, family=device_family,
@ -210,13 +137,13 @@ class OneWireHub:
devices.append(device) devices.append(device)
if device_branches := DEVICE_COUPLERS.get(device_family): if device_branches := DEVICE_COUPLERS.get(device_family):
for branch in device_branches: for branch in device_branches:
devices += self._discover_devices_owserver( devices += self._discover_devices(
f"{device_path}{branch}", device_id f"{device_path}{branch}", device_id
) )
return devices return devices
def _get_device_type_owserver(self, device_path: str) -> str: def _get_device_type(self, device_path: str) -> str:
"""Get device model.""" """Get device model."""
if TYPE_CHECKING: if TYPE_CHECKING:
assert self.owproxy assert self.owproxy

View file

@ -1,16 +1,13 @@
"""Support for 1-Wire environment sensors.""" """Support for 1-Wire environment sensors."""
from __future__ import annotations from __future__ import annotations
import asyncio
from collections.abc import Callable, Mapping from collections.abc import Callable, Mapping
import copy import copy
from dataclasses import dataclass from dataclasses import dataclass
import logging import logging
import os import os
from types import MappingProxyType from types import MappingProxyType
from typing import TYPE_CHECKING, Any from typing import Any
from pi1wire import InvalidCRCException, OneWireInterface, UnsupportResponseException
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
@ -20,7 +17,6 @@ from homeassistant.components.sensor import (
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_TYPE,
ELECTRIC_POTENTIAL_VOLT, ELECTRIC_POTENTIAL_VOLT,
LIGHT_LUX, LIGHT_LUX,
PERCENTAGE, PERCENTAGE,
@ -29,13 +25,10 @@ from homeassistant.const import (
TEMP_CELSIUS, TEMP_CELSIUS,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
from .const import ( from .const import (
CONF_TYPE_OWSERVER,
CONF_TYPE_SYSBUS,
DEVICE_KEYS_0_3, DEVICE_KEYS_0_3,
DEVICE_KEYS_A_B, DEVICE_KEYS_A_B,
DOMAIN, DOMAIN,
@ -45,12 +38,7 @@ from .const import (
READ_MODE_FLOAT, READ_MODE_FLOAT,
READ_MODE_INT, READ_MODE_INT,
) )
from .model import OWDirectDeviceDescription, OWServerDeviceDescription from .onewire_entities import OneWireEntity, OneWireEntityDescription
from .onewire_entities import (
OneWireBaseEntity,
OneWireEntityDescription,
OneWireProxyEntity,
)
from .onewirehub import OneWireHub from .onewirehub import OneWireHub
@ -263,8 +251,6 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = {
} }
# EF sensors are usually hobbyboards specialized sensors. # EF sensors are usually hobbyboards specialized sensors.
# These can only be read by OWFS. Currently this driver only supports them
# via owserver (network protocol)
HOBBYBOARD_EF: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { HOBBYBOARD_EF: dict[str, tuple[OneWireSensorEntityDescription, ...]] = {
"HobbyBoards_EF": ( "HobbyBoards_EF": (
@ -383,109 +369,72 @@ async def async_setup_entry(
"""Set up 1-Wire platform.""" """Set up 1-Wire platform."""
onewirehub = hass.data[DOMAIN][config_entry.entry_id] onewirehub = hass.data[DOMAIN][config_entry.entry_id]
entities = await hass.async_add_executor_job( entities = await hass.async_add_executor_job(
get_entities, onewirehub, config_entry.data, config_entry.options get_entities, onewirehub, config_entry.options
) )
async_add_entities(entities, True) async_add_entities(entities, True)
def get_entities( def get_entities(
onewirehub: OneWireHub, onewirehub: OneWireHub, options: MappingProxyType[str, Any]
config: MappingProxyType[str, Any], ) -> list[OneWireSensor]:
options: MappingProxyType[str, Any],
) -> list[SensorEntity]:
"""Get a list of entities.""" """Get a list of entities."""
if not onewirehub.devices: if not onewirehub.devices:
return [] return []
entities: list[SensorEntity] = [] entities: list[OneWireSensor] = []
conf_type = config[CONF_TYPE] assert onewirehub.owproxy
# We have an owserver on a remote(or local) host/port for device in onewirehub.devices:
if conf_type == CONF_TYPE_OWSERVER: family = device.family
assert onewirehub.owproxy device_type = device.type
for device in onewirehub.devices: device_id = device.id
if TYPE_CHECKING: device_info = device.device_info
assert isinstance(device, OWServerDeviceDescription) device_sub_type = "std"
family = device.family device_path = device.path
device_type = device.type if "EF" in family:
device_id = device.id device_sub_type = "HobbyBoard"
device_info = device.device_info family = device_type
device_sub_type = "std" elif "7E" in family:
device_path = device.path device_sub_type = "EDS"
if "EF" in family: family = device_type
device_sub_type = "HobbyBoard"
family = device_type
elif "7E" in family:
device_sub_type = "EDS"
family = device_type
if family not in get_sensor_types(device_sub_type): if family not in get_sensor_types(device_sub_type):
continue continue
for description in get_sensor_types(device_sub_type)[family]: for description in get_sensor_types(device_sub_type)[family]:
if description.key.startswith("moisture/"): if description.key.startswith("moisture/"):
s_id = description.key.split(".")[1] s_id = description.key.split(".")[1]
is_leaf = int( is_leaf = int(
onewirehub.owproxy.read( onewirehub.owproxy.read(
f"{device_path}moisture/is_leaf.{s_id}" f"{device_path}moisture/is_leaf.{s_id}"
).decode() ).decode()
)
if is_leaf:
description = copy.deepcopy(description)
description.device_class = SensorDeviceClass.HUMIDITY
description.native_unit_of_measurement = PERCENTAGE
description.name = f"Wetness {s_id}"
override_key = None
if description.override_key:
override_key = description.override_key(device_id, options)
device_file = os.path.join(
os.path.split(device.path)[0],
override_key or description.key,
) )
name = f"{device_id} {description.name}" if is_leaf:
entities.append( description = copy.deepcopy(description)
OneWireProxySensor( description.device_class = SensorDeviceClass.HUMIDITY
description=description, description.native_unit_of_measurement = PERCENTAGE
device_id=device_id, description.name = f"Wetness {s_id}"
device_file=device_file, override_key = None
device_info=device_info, if description.override_key:
name=name, override_key = description.override_key(device_id, options)
owproxy=onewirehub.owproxy, device_file = os.path.join(
) os.path.split(device.path)[0],
) override_key or description.key,
)
# We have a raw GPIO ow sensor on a Pi
elif conf_type == CONF_TYPE_SYSBUS:
for device in onewirehub.devices:
if TYPE_CHECKING:
assert isinstance(device, OWDirectDeviceDescription)
p1sensor: OneWireInterface = device.interface
family = p1sensor.mac_address[:2]
device_id = f"{family}-{p1sensor.mac_address[2:]}"
device_info = device.device_info
description = SIMPLE_TEMPERATURE_SENSOR_DESCRIPTION
device_file = f"/sys/bus/w1/devices/{device_id}/w1_slave"
name = f"{device_id} {description.name}" name = f"{device_id} {description.name}"
entities.append( entities.append(
OneWireDirectSensor( OneWireSensor(
description=description, description=description,
device_id=device_id, device_id=device_id,
device_file=device_file, device_file=device_file,
device_info=device_info, device_info=device_info,
name=name, name=name,
owsensor=p1sensor, owproxy=onewirehub.owproxy,
) )
) )
return entities return entities
class OneWireSensor(OneWireBaseEntity, SensorEntity): class OneWireSensor(OneWireEntity, SensorEntity):
"""Mixin for sensor specific attributes.""" """Implementation of a 1-Wire sensor."""
entity_description: OneWireSensorEntityDescription
class OneWireProxySensor(OneWireProxyEntity, OneWireSensor):
"""Implementation of a 1-Wire sensor connected through owserver."""
entity_description: OneWireSensorEntityDescription entity_description: OneWireSensorEntityDescription
@ -493,69 +442,3 @@ class OneWireProxySensor(OneWireProxyEntity, OneWireSensor):
def native_value(self) -> StateType: def native_value(self) -> StateType:
"""Return the state of the entity.""" """Return the state of the entity."""
return self._state return self._state
class OneWireDirectSensor(OneWireSensor):
"""Implementation of a 1-Wire sensor directly connected to RPI GPIO."""
def __init__(
self,
description: OneWireSensorEntityDescription,
device_id: str,
device_info: DeviceInfo,
device_file: str,
name: str,
owsensor: OneWireInterface,
) -> None:
"""Initialize the sensor."""
super().__init__(
description=description,
device_id=device_id,
device_info=device_info,
device_file=device_file,
name=name,
)
self._attr_unique_id = device_file
self._owsensor = owsensor
@property
def native_value(self) -> StateType:
"""Return the state of the entity."""
return self._state
async def get_temperature(self) -> float:
"""Get the latest data from the device."""
attempts = 1
while True:
try:
return await self.hass.async_add_executor_job(
self._owsensor.get_temperature
)
except UnsupportResponseException as ex:
_LOGGER.debug(
"Cannot read from sensor %s (retry attempt %s): %s",
self._device_file,
attempts,
ex,
)
await asyncio.sleep(0.2)
attempts += 1
if attempts > 10:
raise
async def async_update(self) -> None:
"""Get the latest data from the device."""
try:
self._value_raw = await self.get_temperature()
self._state = round(self._value_raw, 1)
except (
FileNotFoundError,
InvalidCRCException,
UnsupportResponseException,
) as ex:
_LOGGER.warning(
"Cannot read from sensor %s: %s",
self._device_file,
ex,
)
self._state = None

View file

@ -4,22 +4,15 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]" "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}, },
"error": { "error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
"invalid_path": "Directory not found."
}, },
"step": { "step": {
"owserver": { "user": {
"data": { "data": {
"host": "[%key:common::config_flow::data::host%]", "host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]" "port": "[%key:common::config_flow::data::port%]"
}, },
"title": "Set owserver details" "title": "Set server details"
},
"user": {
"data": {
"type": "Connection type"
},
"title": "Set up 1-Wire"
} }
} }
}, },
@ -28,11 +21,6 @@
"device_not_selected": "Select devices to configure" "device_not_selected": "Select devices to configure"
}, },
"step": { "step": {
"ack_no_options": {
"data": {},
"description": "There are no options for the SysBus implementation",
"title": "OneWire SysBus Options"
},
"device_selection": { "device_selection": {
"data": { "data": {
"clear_device_options": "Clear all device configurations", "clear_device_options": "Clear all device configurations",

View file

@ -3,25 +3,22 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
import os import os
from typing import TYPE_CHECKING, Any from typing import Any
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_TYPE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import ( from .const import (
CONF_TYPE_OWSERVER,
DEVICE_KEYS_0_3, DEVICE_KEYS_0_3,
DEVICE_KEYS_0_7, DEVICE_KEYS_0_7,
DEVICE_KEYS_A_B, DEVICE_KEYS_A_B,
DOMAIN, DOMAIN,
READ_MODE_BOOL, READ_MODE_BOOL,
) )
from .model import OWServerDeviceDescription from .onewire_entities import OneWireEntity, OneWireEntityDescription
from .onewire_entities import OneWireEntityDescription, OneWireProxyEntity
from .onewirehub import OneWireHub from .onewirehub import OneWireHub
@ -153,24 +150,20 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up 1-Wire platform.""" """Set up 1-Wire platform."""
# Only OWServer implementation works with switches onewirehub = hass.data[DOMAIN][config_entry.entry_id]
if config_entry.data[CONF_TYPE] == CONF_TYPE_OWSERVER:
onewirehub = hass.data[DOMAIN][config_entry.entry_id]
entities = await hass.async_add_executor_job(get_entities, onewirehub) entities = await hass.async_add_executor_job(get_entities, onewirehub)
async_add_entities(entities, True) async_add_entities(entities, True)
def get_entities(onewirehub: OneWireHub) -> list[SwitchEntity]: def get_entities(onewirehub: OneWireHub) -> list[OneWireSwitch]:
"""Get a list of entities.""" """Get a list of entities."""
if not onewirehub.devices: if not onewirehub.devices:
return [] return []
entities: list[SwitchEntity] = [] entities: list[OneWireSwitch] = []
for device in onewirehub.devices: for device in onewirehub.devices:
if TYPE_CHECKING:
assert isinstance(device, OWServerDeviceDescription)
family = device.family family = device.family
device_type = device.type device_type = device.type
device_id = device.id device_id = device.id
@ -186,7 +179,7 @@ def get_entities(onewirehub: OneWireHub) -> list[SwitchEntity]:
device_file = os.path.join(os.path.split(device.path)[0], description.key) device_file = os.path.join(os.path.split(device.path)[0], description.key)
name = f"{device_id} {description.name}" name = f"{device_id} {description.name}"
entities.append( entities.append(
OneWireProxySwitch( OneWireSwitch(
description=description, description=description,
device_id=device_id, device_id=device_id,
device_file=device_file, device_file=device_file,
@ -199,7 +192,7 @@ def get_entities(onewirehub: OneWireHub) -> list[SwitchEntity]:
return entities return entities
class OneWireProxySwitch(OneWireProxyEntity, SwitchEntity): class OneWireSwitch(OneWireEntity, SwitchEntity):
"""Implementation of a 1-Wire switch.""" """Implementation of a 1-Wire switch."""
entity_description: OneWireSwitchEntityDescription entity_description: OneWireSwitchEntityDescription
@ -211,8 +204,8 @@ class OneWireProxySwitch(OneWireProxyEntity, SwitchEntity):
def turn_on(self, **kwargs: Any) -> None: def turn_on(self, **kwargs: Any) -> None:
"""Turn the entity on.""" """Turn the entity on."""
self._write_value_ownet(b"1") self._write_value(b"1")
def turn_off(self, **kwargs: Any) -> None: def turn_off(self, **kwargs: Any) -> None:
"""Turn the entity off.""" """Turn the entity off."""
self._write_value_ownet(b"0") self._write_value(b"0")

View file

@ -1210,9 +1210,6 @@ pexpect==4.6.0
# homeassistant.components.modem_callerid # homeassistant.components.modem_callerid
phone_modem==0.1.1 phone_modem==0.1.1
# homeassistant.components.onewire
pi1wire==0.1.0
# homeassistant.components.remote_rpi_gpio # homeassistant.components.remote_rpi_gpio
pigpio==1.78 pigpio==1.78

View file

@ -812,9 +812,6 @@ pexpect==4.6.0
# homeassistant.components.modem_callerid # homeassistant.components.modem_callerid
phone_modem==0.1.1 phone_modem==0.1.1
# homeassistant.components.onewire
pi1wire==0.1.0
# homeassistant.components.pilight # homeassistant.components.pilight
pilight==0.1.1 pilight==0.1.1

View file

@ -7,7 +7,6 @@ from unittest.mock import MagicMock
from pyownet.protocol import ProtocolError from pyownet.protocol import ProtocolError
from homeassistant.components.onewire.const import DEFAULT_SYSBUS_MOUNT_DIR
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
ATTR_IDENTIFIERS, ATTR_IDENTIFIERS,
@ -29,7 +28,6 @@ from .const import (
ATTR_UNIQUE_ID, ATTR_UNIQUE_ID,
FIXED_ATTRIBUTES, FIXED_ATTRIBUTES,
MOCK_OWPROXY_DEVICES, MOCK_OWPROXY_DEVICES,
MOCK_SYSBUS_DEVICES,
) )
@ -181,30 +179,3 @@ def _setup_owproxy_mock_device_reads(
device_sensors = mock_device.get(platform, []) device_sensors = mock_device.get(platform, [])
for expected_sensor in device_sensors: for expected_sensor in device_sensors:
sub_read_side_effect.append(expected_sensor[ATTR_INJECT_READS]) sub_read_side_effect.append(expected_sensor[ATTR_INJECT_READS])
def setup_sysbus_mock_devices(
platform: str, device_ids: list[str]
) -> tuple[list[str], list[Any]]:
"""Set up mock for sysbus."""
glob_result = []
read_side_effect = []
for device_id in device_ids:
mock_device = MOCK_SYSBUS_DEVICES[device_id]
# Setup directory listing
glob_result += [f"/{DEFAULT_SYSBUS_MOUNT_DIR}/{device_id}"]
# Setup sub-device reads
device_sensors = mock_device.get(platform, [])
for expected_sensor in device_sensors:
if isinstance(expected_sensor[ATTR_INJECT_READS], list):
read_side_effect += expected_sensor[ATTR_INJECT_READS]
else:
read_side_effect.append(expected_sensor[ATTR_INJECT_READS])
# Ensure enough read side effect
read_side_effect.extend([FileNotFoundError("Missing injected value")] * 20)
return (glob_result, read_side_effect)

View file

@ -4,15 +4,9 @@ from unittest.mock import MagicMock, patch
from pyownet.protocol import ConnError from pyownet.protocol import ConnError
import pytest import pytest
from homeassistant.components.onewire.const import ( from homeassistant.components.onewire.const import DOMAIN
CONF_MOUNT_DIR,
CONF_TYPE_OWSERVER,
CONF_TYPE_SYSBUS,
DEFAULT_SYSBUS_MOUNT_DIR,
DOMAIN,
)
from homeassistant.config_entries import SOURCE_USER, ConfigEntry from homeassistant.config_entries import SOURCE_USER, ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .const import MOCK_OWPROXY_DEVICES from .const import MOCK_OWPROXY_DEVICES
@ -33,7 +27,6 @@ def get_config_entry(hass: HomeAssistant) -> ConfigEntry:
domain=DOMAIN, domain=DOMAIN,
source=SOURCE_USER, source=SOURCE_USER,
data={ data={
CONF_TYPE: CONF_TYPE_OWSERVER,
CONF_HOST: "1.2.3.4", CONF_HOST: "1.2.3.4",
CONF_PORT: 1234, CONF_PORT: 1234,
}, },
@ -49,24 +42,6 @@ def get_config_entry(hass: HomeAssistant) -> ConfigEntry:
return config_entry return config_entry
@pytest.fixture(name="sysbus_config_entry")
def get_sysbus_config_entry(hass: HomeAssistant) -> ConfigEntry:
"""Create and register mock config entry."""
config_entry = MockConfigEntry(
domain=DOMAIN,
source=SOURCE_USER,
data={
CONF_TYPE: CONF_TYPE_SYSBUS,
CONF_MOUNT_DIR: DEFAULT_SYSBUS_MOUNT_DIR,
},
unique_id=f"{CONF_TYPE_SYSBUS}:{DEFAULT_SYSBUS_MOUNT_DIR}",
options={},
entry_id="3",
)
config_entry.add_to_hass(hass)
return config_entry
@pytest.fixture(name="owproxy") @pytest.fixture(name="owproxy")
def get_owproxy() -> MagicMock: def get_owproxy() -> MagicMock:
"""Mock owproxy.""" """Mock owproxy."""
@ -82,12 +57,3 @@ def get_owproxy_with_connerror() -> MagicMock:
side_effect=ConnError, side_effect=ConnError,
) as owproxy: ) as owproxy:
yield owproxy yield owproxy
@pytest.fixture(name="sysbus")
def get_sysbus() -> MagicMock:
"""Mock sysbus."""
with patch(
"homeassistant.components.onewire.onewirehub.os.path.isdir", return_value=True
):
yield

View file

@ -1,5 +1,4 @@
"""Constants for 1-Wire integration.""" """Constants for 1-Wire integration."""
from pi1wire import InvalidCRCException, UnsupportResponseException
from pyownet.protocol import Error as ProtocolError from pyownet.protocol import Error as ProtocolError
from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.binary_sensor import BinarySensorDeviceClass
@ -1131,142 +1130,3 @@ MOCK_OWPROXY_DEVICES = {
], ],
}, },
} }
MOCK_SYSBUS_DEVICES = {
"00-111111111111": {
ATTR_UNKNOWN_DEVICE: True,
},
"10-111111111111": {
ATTR_DEVICE_INFO: {
ATTR_IDENTIFIERS: {(DOMAIN, "10-111111111111")},
ATTR_MANUFACTURER: MANUFACTURER_MAXIM,
ATTR_MODEL: "10",
ATTR_NAME: "10-111111111111",
},
Platform.SENSOR: [
{
ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE,
ATTR_ENTITY_ID: "sensor.10_111111111111_temperature",
ATTR_INJECT_READS: 25.123,
ATTR_STATE: "25.1",
ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT,
ATTR_UNIQUE_ID: "/sys/bus/w1/devices/10-111111111111/w1_slave",
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS,
},
],
},
"22-111111111111": {
ATTR_DEVICE_INFO: {
ATTR_IDENTIFIERS: {(DOMAIN, "22-111111111111")},
ATTR_MANUFACTURER: MANUFACTURER_MAXIM,
ATTR_MODEL: "22",
ATTR_NAME: "22-111111111111",
},
Platform.SENSOR: [
{
ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE,
ATTR_ENTITY_ID: "sensor.22_111111111111_temperature",
ATTR_INJECT_READS: FileNotFoundError,
ATTR_STATE: STATE_UNKNOWN,
ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT,
ATTR_UNIQUE_ID: "/sys/bus/w1/devices/22-111111111111/w1_slave",
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS,
},
],
},
"28-111111111111": {
ATTR_DEVICE_INFO: {
ATTR_IDENTIFIERS: {(DOMAIN, "28-111111111111")},
ATTR_MANUFACTURER: MANUFACTURER_MAXIM,
ATTR_MODEL: "28",
ATTR_NAME: "28-111111111111",
},
Platform.SENSOR: [
{
ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE,
ATTR_ENTITY_ID: "sensor.28_111111111111_temperature",
ATTR_INJECT_READS: InvalidCRCException,
ATTR_STATE: STATE_UNKNOWN,
ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT,
ATTR_UNIQUE_ID: "/sys/bus/w1/devices/28-111111111111/w1_slave",
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS,
},
],
},
"3B-111111111111": {
ATTR_DEVICE_INFO: {
ATTR_IDENTIFIERS: {(DOMAIN, "3B-111111111111")},
ATTR_MANUFACTURER: MANUFACTURER_MAXIM,
ATTR_MODEL: "3B",
ATTR_NAME: "3B-111111111111",
},
Platform.SENSOR: [
{
ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE,
ATTR_ENTITY_ID: "sensor.3b_111111111111_temperature",
ATTR_INJECT_READS: 29.993,
ATTR_STATE: "30.0",
ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT,
ATTR_UNIQUE_ID: "/sys/bus/w1/devices/3B-111111111111/w1_slave",
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS,
},
],
},
"42-111111111111": {
ATTR_DEVICE_INFO: {
ATTR_IDENTIFIERS: {(DOMAIN, "42-111111111111")},
ATTR_MANUFACTURER: MANUFACTURER_MAXIM,
ATTR_MODEL: "42",
ATTR_NAME: "42-111111111111",
},
Platform.SENSOR: [
{
ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE,
ATTR_ENTITY_ID: "sensor.42_111111111111_temperature",
ATTR_INJECT_READS: UnsupportResponseException,
ATTR_STATE: STATE_UNKNOWN,
ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT,
ATTR_UNIQUE_ID: "/sys/bus/w1/devices/42-111111111111/w1_slave",
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS,
},
],
},
"42-111111111112": {
ATTR_DEVICE_INFO: {
ATTR_IDENTIFIERS: {(DOMAIN, "42-111111111112")},
ATTR_MANUFACTURER: MANUFACTURER_MAXIM,
ATTR_MODEL: "42",
ATTR_NAME: "42-111111111112",
},
Platform.SENSOR: [
{
ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE,
ATTR_ENTITY_ID: "sensor.42_111111111112_temperature",
ATTR_INJECT_READS: [UnsupportResponseException] * 9 + [27.993],
ATTR_STATE: "28.0",
ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT,
ATTR_UNIQUE_ID: "/sys/bus/w1/devices/42-111111111112/w1_slave",
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS,
},
],
},
"42-111111111113": {
ATTR_DEVICE_INFO: {
ATTR_IDENTIFIERS: {(DOMAIN, "42-111111111113")},
ATTR_MANUFACTURER: MANUFACTURER_MAXIM,
ATTR_MODEL: "42",
ATTR_NAME: "42-111111111113",
},
Platform.SENSOR: [
{
ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE,
ATTR_ENTITY_ID: "sensor.42_111111111113_temperature",
ATTR_INJECT_READS: [UnsupportResponseException] * 10 + [27.993],
ATTR_STATE: STATE_UNKNOWN,
ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT,
ATTR_UNIQUE_ID: "/sys/bus/w1/devices/42-111111111113/w1_slave",
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS,
},
],
},
}

View file

@ -1,4 +1,4 @@
"""Tests for 1-Wire devices connected on OWServer.""" """Tests for 1-Wire binary sensors."""
import logging import logging
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
@ -27,7 +27,7 @@ def override_platforms():
yield yield
async def test_owserver_binary_sensor( async def test_binary_sensors(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: ConfigEntry,
owproxy: MagicMock, owproxy: MagicMock,

View file

@ -4,15 +4,9 @@ from unittest.mock import AsyncMock, patch
from pyownet import protocol from pyownet import protocol
import pytest import pytest
from homeassistant.components.onewire.const import ( from homeassistant.components.onewire.const import DOMAIN
CONF_MOUNT_DIR,
CONF_TYPE_OWSERVER,
CONF_TYPE_SYSBUS,
DEFAULT_SYSBUS_MOUNT_DIR,
DOMAIN,
)
from homeassistant.config_entries import SOURCE_USER, ConfigEntry from homeassistant.config_entries import SOURCE_USER, ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import ( from homeassistant.data_entry_flow import (
RESULT_TYPE_ABORT, RESULT_TYPE_ABORT,
@ -30,23 +24,14 @@ def override_async_setup_entry() -> AsyncMock:
yield mock_setup_entry yield mock_setup_entry
async def test_user_owserver(hass: HomeAssistant, mock_setup_entry: AsyncMock): async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock):
"""Test OWServer user flow.""" """Test user flow."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER} DOMAIN, context={"source": SOURCE_USER}
) )
assert result["type"] == RESULT_TYPE_FORM assert result["type"] == RESULT_TYPE_FORM
assert not result["errors"] assert not result["errors"]
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_TYPE: CONF_TYPE_OWSERVER},
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "owserver"
assert not result["errors"]
# Invalid server # Invalid server
with patch( with patch(
"homeassistant.components.onewire.onewirehub.protocol.proxy", "homeassistant.components.onewire.onewirehub.protocol.proxy",
@ -58,7 +43,7 @@ async def test_user_owserver(hass: HomeAssistant, mock_setup_entry: AsyncMock):
) )
assert result["type"] == RESULT_TYPE_FORM assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "owserver" assert result["step_id"] == "user"
assert result["errors"] == {"base": "cannot_connect"} assert result["errors"] == {"base": "cannot_connect"}
# Valid server # Valid server
@ -73,7 +58,6 @@ async def test_user_owserver(hass: HomeAssistant, mock_setup_entry: AsyncMock):
assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "1.2.3.4" assert result["title"] == "1.2.3.4"
assert result["data"] == { assert result["data"] == {
CONF_TYPE: CONF_TYPE_OWSERVER,
CONF_HOST: "1.2.3.4", CONF_HOST: "1.2.3.4",
CONF_PORT: 1234, CONF_PORT: 1234,
} }
@ -81,10 +65,10 @@ async def test_user_owserver(hass: HomeAssistant, mock_setup_entry: AsyncMock):
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
async def test_user_owserver_duplicate( async def test_user_duplicate(
hass: HomeAssistant, config_entry: ConfigEntry, mock_setup_entry: AsyncMock hass: HomeAssistant, config_entry: ConfigEntry, mock_setup_entry: AsyncMock
): ):
"""Test OWServer flow.""" """Test user duplicate flow."""
await hass.config_entries.async_setup(config_entry.entry_id) await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(hass.config_entries.async_entries(DOMAIN)) == 1
@ -93,15 +77,7 @@ async def test_user_owserver_duplicate(
DOMAIN, context={"source": SOURCE_USER} DOMAIN, context={"source": SOURCE_USER}
) )
assert result["type"] == RESULT_TYPE_FORM assert result["type"] == RESULT_TYPE_FORM
assert not result["errors"] assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_TYPE: CONF_TYPE_OWSERVER},
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "owserver"
assert not result["errors"] assert not result["errors"]
# Duplicate server # Duplicate server
@ -113,93 +89,3 @@ async def test_user_owserver_duplicate(
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured"
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
async def test_user_sysbus(hass: HomeAssistant, mock_setup_entry: AsyncMock):
"""Test SysBus flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_FORM
assert not result["errors"]
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_TYPE: CONF_TYPE_SYSBUS},
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "mount_dir"
assert not result["errors"]
# Invalid path
with patch(
"homeassistant.components.onewire.onewirehub.os.path.isdir",
return_value=False,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_MOUNT_DIR: "/sys/bus/invalid_directory"},
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "mount_dir"
assert result["errors"] == {"base": "invalid_path"}
# Valid path
with patch(
"homeassistant.components.onewire.onewirehub.os.path.isdir",
return_value=True,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_MOUNT_DIR: "/sys/bus/directory"},
)
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "/sys/bus/directory"
assert result["data"] == {
CONF_TYPE: CONF_TYPE_SYSBUS,
CONF_MOUNT_DIR: "/sys/bus/directory",
}
await hass.async_block_till_done()
assert len(mock_setup_entry.mock_calls) == 1
async def test_user_sysbus_duplicate(
hass: HomeAssistant, sysbus_config_entry: ConfigEntry, mock_setup_entry: AsyncMock
):
"""Test SysBus duplicate flow."""
await hass.config_entries.async_setup(sysbus_config_entry.entry_id)
await hass.async_block_till_done()
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_FORM
assert not result["errors"]
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_TYPE: CONF_TYPE_SYSBUS},
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "mount_dir"
assert not result["errors"]
# Valid path
with patch(
"homeassistant.components.onewire.onewirehub.os.path.isdir",
return_value=True,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_MOUNT_DIR: DEFAULT_SYSBUS_MOUNT_DIR},
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
await hass.async_block_till_done()
assert len(mock_setup_entry.mock_calls) == 1

View file

@ -52,7 +52,6 @@ async def test_entry_diagnostics(
"data": { "data": {
"host": REDACTED, "host": REDACTED,
"port": 1234, "port": 1234,
"type": "OWServer",
}, },
"options": { "options": {
"device_options": { "device_options": {

View file

@ -1,5 +1,4 @@
"""Tests for 1-Wire config flow.""" """Tests for 1-Wire config flow."""
import logging
from unittest.mock import MagicMock from unittest.mock import MagicMock
from pyownet import protocol from pyownet import protocol
@ -11,7 +10,7 @@ from homeassistant.core import HomeAssistant
@pytest.mark.usefixtures("owproxy_with_connerror") @pytest.mark.usefixtures("owproxy_with_connerror")
async def test_owserver_connect_failure(hass: HomeAssistant, config_entry: ConfigEntry): async def test_connect_failure(hass: HomeAssistant, config_entry: ConfigEntry):
"""Test connection failure raises ConfigEntryNotReady.""" """Test connection failure raises ConfigEntryNotReady."""
await hass.config_entries.async_setup(config_entry.entry_id) await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -21,7 +20,7 @@ async def test_owserver_connect_failure(hass: HomeAssistant, config_entry: Confi
assert not hass.data.get(DOMAIN) assert not hass.data.get(DOMAIN)
async def test_owserver_listing_failure( async def test_listing_failure(
hass: HomeAssistant, config_entry: ConfigEntry, owproxy: MagicMock hass: HomeAssistant, config_entry: ConfigEntry, owproxy: MagicMock
): ):
"""Test listing failure raises ConfigEntryNotReady.""" """Test listing failure raises ConfigEntryNotReady."""
@ -49,34 +48,3 @@ async def test_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
assert config_entry.state is ConfigEntryState.NOT_LOADED assert config_entry.state is ConfigEntryState.NOT_LOADED
assert not hass.data.get(DOMAIN) assert not hass.data.get(DOMAIN)
@pytest.mark.usefixtures("sysbus")
async def test_warning_no_devices(
hass: HomeAssistant,
sysbus_config_entry: ConfigEntry,
caplog: pytest.LogCaptureFixture,
):
"""Test warning is generated when no sysbus devices found."""
with caplog.at_level(logging.WARNING, logger="homeassistant.components.onewire"):
await hass.config_entries.async_setup(sysbus_config_entry.entry_id)
await hass.async_block_till_done()
assert "No onewire sensor found. Check if dtoverlay=w1-gpio" in caplog.text
@pytest.mark.usefixtures("sysbus")
async def test_unload_sysbus_entry(
hass: HomeAssistant, sysbus_config_entry: ConfigEntry
):
"""Test being able to unload an entry."""
await hass.config_entries.async_setup(sysbus_config_entry.entry_id)
await hass.async_block_till_done()
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert sysbus_config_entry.state is ConfigEntryState.LOADED
assert await hass.config_entries.async_unload(sysbus_config_entry.entry_id)
await hass.async_block_till_done()
assert sysbus_config_entry.state is ConfigEntryState.NOT_LOADED
assert not hass.data.get(DOMAIN)

View file

@ -2,8 +2,6 @@
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from homeassistant.components.onewire.const import ( from homeassistant.components.onewire.const import (
CONF_TYPE_SYSBUS,
DOMAIN,
INPUT_ENTRY_CLEAR_OPTIONS, INPUT_ENTRY_CLEAR_OPTIONS,
INPUT_ENTRY_DEVICE_SELECTION, INPUT_ENTRY_DEVICE_SELECTION,
) )
@ -26,13 +24,7 @@ class FakeDevice:
name_by_user = "Given Name" name_by_user = "Given Name"
class FakeOWHubSysBus: async def test_user_options_clear(
"""Mock Class for mocking onewire hub."""
type = CONF_TYPE_SYSBUS
async def test_user_owserver_options_clear(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: ConfigEntry,
owproxy: MagicMock, owproxy: MagicMock,
@ -61,7 +53,7 @@ async def test_user_owserver_options_clear(
assert result["data"] == {} assert result["data"] == {}
async def test_user_owserver_options_empty_selection( async def test_user_options_empty_selection(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: ConfigEntry,
owproxy: MagicMock, owproxy: MagicMock,
@ -91,7 +83,7 @@ async def test_user_owserver_options_empty_selection(
assert result["errors"] == {"base": "device_not_selected"} assert result["errors"] == {"base": "device_not_selected"}
async def test_user_owserver_options_set_single( async def test_user_options_set_single(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: ConfigEntry,
owproxy: MagicMock, owproxy: MagicMock,
@ -134,7 +126,7 @@ async def test_user_owserver_options_set_single(
) )
async def test_user_owserver_options_set_multiple( async def test_user_options_set_multiple(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: ConfigEntry,
owproxy: MagicMock, owproxy: MagicMock,
@ -208,7 +200,7 @@ async def test_user_owserver_options_set_multiple(
) )
async def test_user_owserver_options_no_devices( async def test_user_options_no_devices(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: ConfigEntry,
owproxy: MagicMock, owproxy: MagicMock,
@ -223,15 +215,3 @@ async def test_user_owserver_options_no_devices(
await hass.async_block_till_done() await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_ABORT assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "No configurable devices found." assert result["reason"] == "No configurable devices found."
async def test_user_sysbus_options(
hass: HomeAssistant,
config_entry: ConfigEntry,
):
"""Test that SysBus options flow aborts on init."""
hass.data[DOMAIN] = {config_entry.entry_id: FakeOWHubSysBus()}
result = await hass.config_entries.options.async_init(config_entry.entry_id)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "SysBus setup does not have any config options."

View file

@ -1,4 +1,4 @@
"""Tests for 1-Wire sensor platform.""" """Tests for 1-Wire sensors."""
import logging import logging
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
@ -14,14 +14,8 @@ from . import (
check_device_registry, check_device_registry,
check_entities, check_entities,
setup_owproxy_mock_devices, setup_owproxy_mock_devices,
setup_sysbus_mock_devices,
)
from .const import (
ATTR_DEVICE_INFO,
ATTR_UNKNOWN_DEVICE,
MOCK_OWPROXY_DEVICES,
MOCK_SYSBUS_DEVICES,
) )
from .const import ATTR_DEVICE_INFO, ATTR_UNKNOWN_DEVICE, MOCK_OWPROXY_DEVICES
from tests.common import mock_device_registry, mock_registry from tests.common import mock_device_registry, mock_registry
@ -33,7 +27,7 @@ def override_platforms():
yield yield
async def test_owserver_sensor( async def test_sensors(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: ConfigEntry,
owproxy: MagicMock, owproxy: MagicMock,
@ -73,44 +67,3 @@ async def test_owserver_sensor(
await hass.async_block_till_done() await hass.async_block_till_done()
check_entities(hass, entity_registry, expected_entities) check_entities(hass, entity_registry, expected_entities)
@pytest.mark.usefixtures("sysbus")
@pytest.mark.parametrize("device_id", MOCK_SYSBUS_DEVICES.keys(), indirect=True)
async def test_onewiredirect_setup_valid_device(
hass: HomeAssistant,
sysbus_config_entry: ConfigEntry,
device_id: str,
caplog: pytest.LogCaptureFixture,
):
"""Test that sysbus config entry works correctly."""
device_registry = mock_device_registry(hass)
entity_registry = mock_registry(hass)
glob_result, read_side_effect = setup_sysbus_mock_devices(
Platform.SENSOR, [device_id]
)
mock_device = MOCK_SYSBUS_DEVICES[device_id]
expected_entities = mock_device.get(Platform.SENSOR, [])
expected_devices = ensure_list(mock_device.get(ATTR_DEVICE_INFO))
with patch("pi1wire._finder.glob.glob", return_value=glob_result,), patch(
"pi1wire.OneWire.get_temperature",
side_effect=read_side_effect,
), caplog.at_level(
logging.WARNING, logger="homeassistant.components.onewire"
), patch(
"homeassistant.components.onewire.sensor.asyncio.sleep"
):
await hass.config_entries.async_setup(sysbus_config_entry.entry_id)
await hass.async_block_till_done()
assert "No onewire sensor found. Check if dtoverlay=w1-gpio" not in caplog.text
if mock_device.get(ATTR_UNKNOWN_DEVICE):
assert "Ignoring unknown device family" in caplog.text
else:
assert "Ignoring unknown device family" not in caplog.text
check_device_registry(device_registry, expected_devices)
assert len(entity_registry.entities) == len(expected_entities)
check_entities(hass, entity_registry, expected_entities)

View file

@ -1,4 +1,4 @@
"""Tests for 1-Wire devices connected on OWServer.""" """Tests for 1-Wire switches."""
import logging import logging
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
@ -35,7 +35,7 @@ def override_platforms():
yield yield
async def test_owserver_switch( async def test_switches(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: ConfigEntry,
owproxy: MagicMock, owproxy: MagicMock,