core/homeassistant/components/onewire/sensor.py

497 lines
16 KiB
Python

"""Support for 1-Wire environment sensors."""
from __future__ import annotations
import asyncio
from glob import glob
import logging
import os
from pi1wire import InvalidCRCException, UnsupportResponseException
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import StateType
from .const import (
CONF_MOUNT_DIR,
CONF_NAMES,
CONF_TYPE_OWFS,
CONF_TYPE_OWSERVER,
CONF_TYPE_SYSBUS,
DEFAULT_OWSERVER_PORT,
DEFAULT_SYSBUS_MOUNT_DIR,
DOMAIN,
SENSOR_TYPE_COUNT,
SENSOR_TYPE_CURRENT,
SENSOR_TYPE_HUMIDITY,
SENSOR_TYPE_ILLUMINANCE,
SENSOR_TYPE_MOISTURE,
SENSOR_TYPE_PRESSURE,
SENSOR_TYPE_TEMPERATURE,
SENSOR_TYPE_VOLTAGE,
SENSOR_TYPE_WETNESS,
)
from .onewire_entities import OneWireBaseEntity, OneWireProxyEntity
from .onewirehub import OneWireHub
_LOGGER = logging.getLogger(__name__)
DEVICE_SENSORS = {
# Family : { SensorType: owfs path }
"10": [
{"path": "temperature", "name": "Temperature", "type": SENSOR_TYPE_TEMPERATURE}
],
"12": [
{
"path": "TAI8570/temperature",
"name": "Temperature",
"type": SENSOR_TYPE_TEMPERATURE,
"default_disabled": True,
},
{
"path": "TAI8570/pressure",
"name": "Pressure",
"type": SENSOR_TYPE_PRESSURE,
"default_disabled": True,
},
],
"22": [
{"path": "temperature", "name": "Temperature", "type": SENSOR_TYPE_TEMPERATURE}
],
"26": [
{"path": "temperature", "name": "Temperature", "type": SENSOR_TYPE_TEMPERATURE},
{
"path": "humidity",
"name": "Humidity",
"type": SENSOR_TYPE_HUMIDITY,
"default_disabled": True,
},
{
"path": "HIH3600/humidity",
"name": "Humidity HIH3600",
"type": SENSOR_TYPE_HUMIDITY,
"default_disabled": True,
},
{
"path": "HIH4000/humidity",
"name": "Humidity HIH4000",
"type": SENSOR_TYPE_HUMIDITY,
"default_disabled": True,
},
{
"path": "HIH5030/humidity",
"name": "Humidity HIH5030",
"type": SENSOR_TYPE_HUMIDITY,
"default_disabled": True,
},
{
"path": "HTM1735/humidity",
"name": "Humidity HTM1735",
"type": SENSOR_TYPE_HUMIDITY,
"default_disabled": True,
},
{
"path": "B1-R1-A/pressure",
"name": "Pressure",
"type": SENSOR_TYPE_PRESSURE,
"default_disabled": True,
},
{
"path": "S3-R1-A/illuminance",
"name": "Illuminance",
"type": SENSOR_TYPE_ILLUMINANCE,
"default_disabled": True,
},
{
"path": "VAD",
"name": "Voltage VAD",
"type": SENSOR_TYPE_VOLTAGE,
"default_disabled": True,
},
{
"path": "VDD",
"name": "Voltage VDD",
"type": SENSOR_TYPE_VOLTAGE,
"default_disabled": True,
},
{
"path": "IAD",
"name": "Current",
"type": SENSOR_TYPE_CURRENT,
"default_disabled": True,
},
],
"28": [
{"path": "temperature", "name": "Temperature", "type": SENSOR_TYPE_TEMPERATURE}
],
"3B": [
{"path": "temperature", "name": "Temperature", "type": SENSOR_TYPE_TEMPERATURE}
],
"42": [
{"path": "temperature", "name": "Temperature", "type": SENSOR_TYPE_TEMPERATURE}
],
"1D": [
{"path": "counter.A", "name": "Counter A", "type": SENSOR_TYPE_COUNT},
{"path": "counter.B", "name": "Counter B", "type": SENSOR_TYPE_COUNT},
],
"EF": [], # "HobbyBoard": special
"7E": [], # "EDS": special
}
DEVICE_SUPPORT_SYSBUS = ["10", "22", "28", "3B", "42"]
# 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 = {
"HobbyBoards_EF": [
{
"path": "humidity/humidity_corrected",
"name": "Humidity",
"type": SENSOR_TYPE_HUMIDITY,
},
{
"path": "humidity/humidity_raw",
"name": "Humidity Raw",
"type": SENSOR_TYPE_HUMIDITY,
},
{
"path": "humidity/temperature",
"name": "Temperature",
"type": SENSOR_TYPE_TEMPERATURE,
},
],
"HB_MOISTURE_METER": [
{
"path": "moisture/sensor.0",
"name": "Moisture 0",
"type": SENSOR_TYPE_MOISTURE,
},
{
"path": "moisture/sensor.1",
"name": "Moisture 1",
"type": SENSOR_TYPE_MOISTURE,
},
{
"path": "moisture/sensor.2",
"name": "Moisture 2",
"type": SENSOR_TYPE_MOISTURE,
},
{
"path": "moisture/sensor.3",
"name": "Moisture 3",
"type": SENSOR_TYPE_MOISTURE,
},
],
}
# 7E sensors are special sensors by Embedded Data Systems
EDS_SENSORS = {
"EDS0068": [
{
"path": "EDS0068/temperature",
"name": "Temperature",
"type": SENSOR_TYPE_TEMPERATURE,
},
{
"path": "EDS0068/pressure",
"name": "Pressure",
"type": SENSOR_TYPE_PRESSURE,
},
{
"path": "EDS0068/light",
"name": "Illuminance",
"type": SENSOR_TYPE_ILLUMINANCE,
},
{
"path": "EDS0068/humidity",
"name": "Humidity",
"type": SENSOR_TYPE_HUMIDITY,
},
],
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_NAMES): {cv.string: cv.string},
vol.Optional(CONF_MOUNT_DIR, default=DEFAULT_SYSBUS_MOUNT_DIR): cv.string,
vol.Optional(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_OWSERVER_PORT): cv.port,
}
)
def get_sensor_types(device_sub_type):
"""Return the proper info array for the device type."""
if "HobbyBoard" in device_sub_type:
return HOBBYBOARD_EF
if "EDS" in device_sub_type:
return EDS_SENSORS
return DEVICE_SENSORS
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Old way of setting up 1-Wire platform."""
if config.get(CONF_HOST):
config[CONF_TYPE] = CONF_TYPE_OWSERVER
elif config[CONF_MOUNT_DIR] == DEFAULT_SYSBUS_MOUNT_DIR:
config[CONF_TYPE] = CONF_TYPE_SYSBUS
else: # pragma: no cover
# This part of the implementation does not conform to policy regarding 3rd-party libraries, and will not longer be updated.
# https://developers.home-assistant.io/docs/creating_platform_code_review/#5-communication-with-devicesservices
config[CONF_TYPE] = CONF_TYPE_OWFS
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
)
)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up 1-Wire platform."""
onewirehub = hass.data[DOMAIN][config_entry.unique_id]
entities = await hass.async_add_executor_job(
get_entities, onewirehub, config_entry.data
)
async_add_entities(entities, True)
def get_entities(onewirehub: OneWireHub, config):
"""Get a list of entities."""
entities = []
device_names = {}
if CONF_NAMES in config and isinstance(config[CONF_NAMES], dict):
device_names = config[CONF_NAMES]
conf_type = config[CONF_TYPE]
# We have an owserver on a remote(or local) host/port
if conf_type == CONF_TYPE_OWSERVER:
for device in onewirehub.devices:
family = device["family"]
device_type = device["type"]
device_id = os.path.split(os.path.split(device["path"])[0])[1]
device_sub_type = "std"
device_path = device["path"]
if "EF" in family:
device_sub_type = "HobbyBoard"
family = device_type
elif "7E" in family:
device_sub_type = "EDS"
family = onewirehub.owproxy.read(f"{device_path}device_type").decode()
if family not in get_sensor_types(device_sub_type):
_LOGGER.warning(
"Ignoring unknown family (%s) of sensor found for device: %s",
family,
device_id,
)
continue
device_info = {
"identifiers": {(DOMAIN, device_id)},
"manufacturer": "Maxim Integrated",
"model": device_type,
"name": device_id,
}
for entity_specs in get_sensor_types(device_sub_type)[family]:
if entity_specs["type"] == SENSOR_TYPE_MOISTURE:
s_id = entity_specs["path"].split(".")[1]
is_leaf = int(
onewirehub.owproxy.read(
f"{device_path}moisture/is_leaf.{s_id}"
).decode()
)
if is_leaf:
entity_specs["type"] = SENSOR_TYPE_WETNESS
entity_specs["name"] = f"Wetness {s_id}"
entity_path = os.path.join(
os.path.split(device_path)[0], entity_specs["path"]
)
entities.append(
OneWireProxySensor(
device_id=device_id,
device_name=device_names.get(device_id, device_id),
device_info=device_info,
entity_path=entity_path,
entity_specs=entity_specs,
owproxy=onewirehub.owproxy,
)
)
# We have a raw GPIO ow sensor on a Pi
elif conf_type == CONF_TYPE_SYSBUS:
base_dir = config[CONF_MOUNT_DIR]
_LOGGER.debug("Initializing using SysBus %s", base_dir)
for p1sensor in onewirehub.devices:
family = p1sensor.mac_address[:2]
sensor_id = f"{family}-{p1sensor.mac_address[2:]}"
if family not in DEVICE_SUPPORT_SYSBUS:
_LOGGER.warning(
"Ignoring unknown family (%s) of sensor found for device: %s",
family,
sensor_id,
)
continue
device_info = {
"identifiers": {(DOMAIN, sensor_id)},
"manufacturer": "Maxim Integrated",
"model": family,
"name": sensor_id,
}
device_file = f"/sys/bus/w1/devices/{sensor_id}/w1_slave"
entities.append(
OneWireDirectSensor(
device_names.get(sensor_id, sensor_id),
device_file,
device_info,
p1sensor,
)
)
if not entities:
_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"
)
# We have an owfs mounted
else: # pragma: no cover
# This part of the implementation does not conform to policy regarding 3rd-party libraries, and will not longer be updated.
# https://developers.home-assistant.io/docs/creating_platform_code_review/#5-communication-with-devicesservices
base_dir = config[CONF_MOUNT_DIR]
_LOGGER.debug("Initializing using OWFS %s", base_dir)
_LOGGER.warning(
"The OWFS implementation of 1-Wire sensors is deprecated, "
"and should be migrated to OWServer (on localhost:4304). "
"If migration to OWServer is not feasible on your installation, "
"please raise an issue at https://github.com/home-assistant/core/issues/new"
"?title=Unable%20to%20migrate%20onewire%20from%20OWFS%20to%20OWServer",
)
for family_file_path in glob(os.path.join(base_dir, "*", "family")):
with open(family_file_path) as family_file:
family = family_file.read()
if "EF" in family:
continue
if family in DEVICE_SENSORS:
for sensor_key, sensor_value in DEVICE_SENSORS[family].items():
sensor_id = os.path.split(os.path.split(family_file_path)[0])[1]
device_file = os.path.join(
os.path.split(family_file_path)[0], sensor_value
)
entities.append(
OneWireOWFSSensor(
device_names.get(sensor_id, sensor_id),
device_file,
sensor_key,
)
)
return entities
class OneWireSensor(OneWireBaseEntity, SensorEntity):
"""Mixin for sensor specific attributes."""
@property
def unit_of_measurement(self) -> str | None:
"""Return the unit the value is expressed in."""
return self._unit_of_measurement
class OneWireProxySensor(OneWireProxyEntity, OneWireSensor):
"""Implementation of a 1-Wire sensor connected through owserver."""
@property
def state(self) -> StateType:
"""Return the state of the entity."""
return self._state
class OneWireDirectSensor(OneWireSensor):
"""Implementation of a 1-Wire sensor directly connected to RPI GPIO."""
def __init__(self, name, device_file, device_info, owsensor):
"""Initialize the sensor."""
super().__init__(name, device_file, "temperature", "Temperature", device_info)
self._owsensor = owsensor
@property
def state(self) -> StateType:
"""Return the state of the entity."""
return self._state
async def get_temperature(self):
"""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):
"""Get the latest data from the device."""
value = None
try:
self._value_raw = await self.get_temperature()
value = round(float(self._value_raw), 1)
except (
FileNotFoundError,
InvalidCRCException,
UnsupportResponseException,
) as ex:
_LOGGER.warning("Cannot read from sensor %s: %s", self._device_file, ex)
self._state = value
class OneWireOWFSSensor(OneWireSensor): # pragma: no cover
"""Implementation of a 1-Wire sensor through owfs.
This part of the implementation does not conform to policy regarding 3rd-party libraries, and will not longer be updated.
https://developers.home-assistant.io/docs/creating_platform_code_review/#5-communication-with-devicesservices
"""
@property
def state(self) -> StateType:
"""Return the state of the entity."""
return self._state
def _read_value_raw(self):
"""Read the value as it is returned by the sensor."""
with open(self._device_file) as ds_device_file:
lines = ds_device_file.readlines()
return lines
def update(self):
"""Get the latest data from the device."""
value = None
try:
value_read = self._read_value_raw()
if len(value_read) == 1:
value = round(float(value_read[0]), 1)
self._value_raw = float(value_read[0])
except ValueError:
_LOGGER.warning("Invalid value read from %s", self._device_file)
except FileNotFoundError:
_LOGGER.warning("Cannot read from sensor: %s", self._device_file)
self._state = value