Firmata analog input, PWM/analog output, deprecate arduino (#40369)

* firmata analog input

* firmata pwm/analog out, use more HA const

* firmata update pymata to 1.19

* deprecate arduino, firmata supersedes it

* firmata sensor diff min, pull review quality changes

* firmata condense platform setup into loop
This commit is contained in:
Perry Naseck 2020-09-22 03:44:16 -04:00 committed by GitHub
parent 50b727ba83
commit 0582bf7746
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 379 additions and 57 deletions

View file

@ -269,7 +269,9 @@ omit =
homeassistant/components/firmata/board.py
homeassistant/components/firmata/const.py
homeassistant/components/firmata/entity.py
homeassistant/components/firmata/light.py
homeassistant/components/firmata/pin.py
homeassistant/components/firmata/sensor.py
homeassistant/components/firmata/switch.py
homeassistant/components/fitbit/sensor.py
homeassistant/components/fixer/sensor.py

View file

@ -23,6 +23,12 @@ CONFIG_SCHEMA = vol.Schema(
def setup(hass, config):
"""Set up the Arduino component."""
_LOGGER.warning(
"The %s integration has been deprecated. Please move your "
"configuration to the firmata integration. "
"https://www.home-assistant.io/integrations/firmata",
DOMAIN,
)
port = config[DOMAIN][CONF_PORT]

View file

@ -6,7 +6,17 @@ import logging
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP
from homeassistant.const import (
CONF_BINARY_SENSORS,
CONF_LIGHTS,
CONF_MAXIMUM,
CONF_MINIMUM,
CONF_NAME,
CONF_PIN,
CONF_SENSORS,
CONF_SWITCHES,
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, device_registry as dr
@ -14,21 +24,22 @@ from .board import FirmataBoard
from .const import (
CONF_ARDUINO_INSTANCE_ID,
CONF_ARDUINO_WAIT,
CONF_BINARY_SENSORS,
CONF_DIFFERENTIAL,
CONF_INITIAL_STATE,
CONF_NEGATE_STATE,
CONF_PIN,
CONF_PIN_MODE,
CONF_PLATFORM_MAP,
CONF_SAMPLING_INTERVAL,
CONF_SERIAL_BAUD_RATE,
CONF_SERIAL_PORT,
CONF_SLEEP_TUNE,
CONF_SWITCHES,
DOMAIN,
FIRMATA_MANUFACTURER,
PIN_MODE_ANALOG,
PIN_MODE_INPUT,
PIN_MODE_OUTPUT,
PIN_MODE_PULLUP,
PIN_MODE_PWM,
)
_LOGGER = logging.getLogger(__name__)
@ -40,8 +51,8 @@ ANALOG_PIN_SCHEMA = vol.All(cv.string, vol.Match(r"^A[0-9]+$"))
SWITCH_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME): cv.string,
# Both digital and analog pins may be used as digital output
vol.Required(CONF_PIN): vol.Any(cv.positive_int, ANALOG_PIN_SCHEMA),
# will be analog mode in future too
vol.Required(CONF_PIN_MODE): PIN_MODE_OUTPUT,
vol.Optional(CONF_INITIAL_STATE, default=False): cv.boolean,
vol.Optional(CONF_NEGATE_STATE, default=False): cv.boolean,
@ -49,17 +60,45 @@ SWITCH_SCHEMA = vol.Schema(
required=True,
)
LIGHT_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME): cv.string,
# Both digital and analog pins may be used as PWM/analog output
vol.Required(CONF_PIN): vol.Any(cv.positive_int, ANALOG_PIN_SCHEMA),
vol.Required(CONF_PIN_MODE): PIN_MODE_PWM,
vol.Optional(CONF_INITIAL_STATE, default=0): cv.positive_int,
vol.Optional(CONF_MINIMUM, default=0): cv.positive_int,
vol.Optional(CONF_MAXIMUM, default=255): cv.positive_int,
},
required=True,
)
BINARY_SENSOR_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME): cv.string,
# Both digital and analog pins may be used as digital input
vol.Required(CONF_PIN): vol.Any(cv.positive_int, ANALOG_PIN_SCHEMA),
# will be analog mode in future too
vol.Required(CONF_PIN_MODE): vol.Any(PIN_MODE_INPUT, PIN_MODE_PULLUP),
vol.Optional(CONF_NEGATE_STATE, default=False): cv.boolean,
},
required=True,
)
SENSOR_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME): cv.string,
# Currently only analog input sensor is implemented
vol.Required(CONF_PIN): ANALOG_PIN_SCHEMA,
vol.Required(CONF_PIN_MODE): PIN_MODE_ANALOG,
# Default differential is 40 to avoid a flood of messages on initial setup
# in case pin is unplugged. Firmata responds really really fast
vol.Optional(CONF_DIFFERENTIAL, default=40): vol.All(
cv.positive_int, vol.Range(min=1)
),
},
required=True,
)
BOARD_CONFIG_SCHEMA = vol.Schema(
{
vol.Required(CONF_SERIAL_PORT): cv.string,
@ -71,7 +110,9 @@ BOARD_CONFIG_SCHEMA = vol.Schema(
),
vol.Optional(CONF_SAMPLING_INTERVAL): cv.positive_int,
vol.Optional(CONF_SWITCHES): [SWITCH_SCHEMA],
vol.Optional(CONF_LIGHTS): [LIGHT_SCHEMA],
vol.Optional(CONF_BINARY_SENSORS): [BINARY_SENSOR_SCHEMA],
vol.Optional(CONF_SENSORS): [SENSOR_SCHEMA],
},
required=True,
)
@ -155,14 +196,11 @@ async def async_setup_entry(
sw_version=board.firmware_version,
)
if CONF_BINARY_SENSORS in config_entry.data:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, "binary_sensor")
)
if CONF_SWITCHES in config_entry.data:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, "switch")
)
for (conf, platform) in CONF_PLATFORM_MAP.items():
if conf in config_entry.data:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, platform)
)
return True
@ -173,16 +211,11 @@ async def async_unload_entry(
_LOGGER.debug("Closing Firmata board %s", config_entry.data[CONF_NAME])
unload_entries = []
if CONF_BINARY_SENSORS in config_entry.data:
unload_entries.append(
hass.config_entries.async_forward_entry_unload(
config_entry, "binary_sensor"
for (conf, platform) in CONF_PLATFORM_MAP.items():
if conf in config_entry.data:
unload_entries.append(
hass.config_entries.async_forward_entry_unload(config_entry, platform)
)
)
if CONF_SWITCHES in config_entry.data:
unload_entries.append(
hass.config_entries.async_forward_entry_unload(config_entry, "switch")
)
results = []
if unload_entries:
results = await asyncio.gather(*unload_entries)

View file

@ -4,10 +4,10 @@ import logging
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME
from homeassistant.const import CONF_NAME, CONF_PIN
from homeassistant.core import HomeAssistant
from .const import CONF_NEGATE_STATE, CONF_PIN, CONF_PIN_MODE, DOMAIN
from .const import CONF_NEGATE_STATE, CONF_PIN_MODE, DOMAIN
from .entity import FirmataPinEntity
from .pin import FirmataBinaryDigitalInput, FirmataPinUsedException
@ -30,7 +30,7 @@ async def async_setup_entry(
api.setup()
except FirmataPinUsedException:
_LOGGER.error(
"Could not setup binary sensor on pin %s since pin already in use.",
"Could not setup binary sensor on pin %s since pin already in use",
binary_sensor[CONF_PIN],
)
continue

View file

@ -5,17 +5,23 @@ from typing import Union
from pymata_express.pymata_express import PymataExpress
from pymata_express.pymata_express_serial import serial
from homeassistant.const import CONF_NAME
from homeassistant.const import (
CONF_BINARY_SENSORS,
CONF_LIGHTS,
CONF_NAME,
CONF_SENSORS,
CONF_SWITCHES,
)
from .const import (
CONF_ARDUINO_INSTANCE_ID,
CONF_ARDUINO_WAIT,
CONF_BINARY_SENSORS,
CONF_SAMPLING_INTERVAL,
CONF_SERIAL_BAUD_RATE,
CONF_SERIAL_PORT,
CONF_SLEEP_TUNE,
CONF_SWITCHES,
PIN_TYPE_ANALOG,
PIN_TYPE_DIGITAL,
)
_LOGGER = logging.getLogger(__name__)
@ -34,13 +40,19 @@ class FirmataBoard:
self.protocol_version = None
self.name = self.config[CONF_NAME]
self.switches = []
self.lights = []
self.binary_sensors = []
self.sensors = []
self.used_pins = []
if CONF_SWITCHES in self.config:
self.switches = self.config[CONF_SWITCHES]
if CONF_LIGHTS in self.config:
self.lights = self.config[CONF_LIGHTS]
if CONF_BINARY_SENSORS in self.config:
self.binary_sensors = self.config[CONF_BINARY_SENSORS]
if CONF_SENSORS in self.config:
self.sensors = self.config[CONF_SENSORS]
async def async_setup(self, tries=0) -> bool:
"""Set up a Firmata instance."""
@ -109,11 +121,11 @@ board %s: %s",
def get_pin_type(self, pin: FirmataPinType) -> tuple:
"""Return the type and Firmata location of a pin on the board."""
if isinstance(pin, str):
pin_type = "analog"
pin_type = PIN_TYPE_ANALOG
firmata_pin = int(pin[1:])
firmata_pin += self.api.first_analog_pin
else:
pin_type = "digital"
pin_type = PIN_TYPE_DIGITAL
firmata_pin = pin
return (pin_type, firmata_pin)

View file

@ -1,24 +1,35 @@
"""Constants for the Firmata component."""
import logging
LOGGER = logging.getLogger(__package__)
from homeassistant.const import (
CONF_BINARY_SENSORS,
CONF_LIGHTS,
CONF_SENSORS,
CONF_SWITCHES,
)
CONF_ARDUINO_INSTANCE_ID = "arduino_instance_id"
CONF_ARDUINO_WAIT = "arduino_wait"
CONF_BINARY_SENSORS = "binary_sensors"
CONF_DIFFERENTIAL = "differential"
CONF_INITIAL_STATE = "initial"
CONF_NAME = "name"
CONF_NEGATE_STATE = "negate"
CONF_PIN = "pin"
CONF_PINS = "pins"
CONF_PIN_MODE = "pin_mode"
PIN_MODE_ANALOG = "ANALOG"
PIN_MODE_OUTPUT = "OUTPUT"
PIN_MODE_PWM = "PWM"
PIN_MODE_INPUT = "INPUT"
PIN_MODE_PULLUP = "PULLUP"
PIN_TYPE_ANALOG = 1
PIN_TYPE_DIGITAL = 0
CONF_SAMPLING_INTERVAL = "sampling_interval"
CONF_SERIAL_BAUD_RATE = "serial_baud_rate"
CONF_SERIAL_PORT = "serial_port"
CONF_SLEEP_TUNE = "sleep_tune"
CONF_SWITCHES = "switches"
DOMAIN = "firmata"
FIRMATA_MANUFACTURER = "Firmata"
CONF_PLATFORM_MAP = {
CONF_BINARY_SENSORS: "binary_sensor",
CONF_LIGHTS: "light",
CONF_SENSORS: "sensor",
CONF_SWITCHES: "switch",
}

View file

@ -0,0 +1,98 @@
"""Support for Firmata light output."""
import logging
from typing import Type
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
SUPPORT_BRIGHTNESS,
LightEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MAXIMUM, CONF_MINIMUM, CONF_NAME, CONF_PIN
from homeassistant.core import HomeAssistant
from .board import FirmataPinType
from .const import CONF_INITIAL_STATE, CONF_PIN_MODE, DOMAIN
from .entity import FirmataPinEntity
from .pin import FirmataBoardPin, FirmataPinUsedException, FirmataPWMOutput
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities
) -> None:
"""Set up the Firmata lights."""
new_entities = []
board = hass.data[DOMAIN][config_entry.entry_id]
for light in board.lights:
pin = light[CONF_PIN]
pin_mode = light[CONF_PIN_MODE]
initial = light[CONF_INITIAL_STATE]
minimum = light[CONF_MINIMUM]
maximum = light[CONF_MAXIMUM]
api = FirmataPWMOutput(board, pin, pin_mode, initial, minimum, maximum)
try:
api.setup()
except FirmataPinUsedException:
_LOGGER.error(
"Could not setup light on pin %s since pin already in use",
light[CONF_PIN],
)
continue
name = light[CONF_NAME]
light_entity = FirmataLight(api, config_entry, name, pin)
new_entities.append(light_entity)
if new_entities:
async_add_entities(new_entities)
class FirmataLight(FirmataPinEntity, LightEntity):
"""Representation of a light on a Firmata board."""
def __init__(
self,
api: Type[FirmataBoardPin],
config_entry: ConfigEntry,
name: str,
pin: FirmataPinType,
):
"""Initialize the light pin entity."""
super().__init__(api, config_entry, name, pin)
# Default first turn on to max
self._last_on_level = 255
async def async_added_to_hass(self) -> None:
"""Set up a light."""
await self._api.start_pin()
@property
def is_on(self) -> bool:
"""Return true if light is on."""
return self._api.state > 0
@property
def brightness(self) -> int:
"""Return the brightness of the light."""
return self._api.state
@property
def supported_features(self) -> int:
"""Flag supported features."""
return SUPPORT_BRIGHTNESS
async def async_turn_on(self, **kwargs) -> None:
"""Turn on light."""
level = kwargs.get(ATTR_BRIGHTNESS, self._last_on_level)
await self._api.set_level(level)
self.async_write_ha_state()
self._last_on_level = level
async def async_turn_off(self, **kwargs) -> None:
"""Turn off light."""
await self._api.set_level(0)
self.async_write_ha_state()

View file

@ -4,7 +4,7 @@
"config_flow": false,
"documentation": "https://www.home-assistant.io/integrations/firmata",
"requirements": [
"pymata-express==1.13"
"pymata-express==1.19"
],
"codeowners": [
"@DaAwesomeP"

View file

@ -2,10 +2,8 @@
import logging
from typing import Callable
from homeassistant.core import callback
from .board import FirmataBoard, FirmataPinType
from .const import PIN_MODE_INPUT, PIN_MODE_PULLUP
from .const import PIN_MODE_INPUT, PIN_MODE_PULLUP, PIN_TYPE_ANALOG
_LOGGER = logging.getLogger(__name__)
@ -25,6 +23,10 @@ class FirmataBoardPin:
self._pin_type, self._firmata_pin = self.board.get_pin_type(self._pin)
self._state = None
if self._pin_type == PIN_TYPE_ANALOG:
# Pymata wants the analog pin formatted as the # from "A#"
self._analog_pin = int(self._pin[1:])
def setup(self):
"""Set up a pin and make sure it is valid."""
if not self.board.mark_pin_used(self._pin):
@ -85,6 +87,53 @@ class FirmataBinaryDigitalOutput(FirmataBoardPin):
self._state = False
class FirmataPWMOutput(FirmataBoardPin):
"""Representation of a Firmata PWM/analog Output Pin."""
def __init__(
self,
board: FirmataBoard,
pin: FirmataPinType,
pin_mode: str,
initial: bool,
minimum: int,
maximum: int,
):
"""Initialize the PWM/analog output pin."""
self._initial = initial
self._min = minimum
self._max = maximum
self._range = self._max - self._min
super().__init__(board, pin, pin_mode)
async def start_pin(self) -> None:
"""Set initial state on a pin."""
_LOGGER.debug(
"Setting initial state for PWM/analog output pin %s on board %s to %d",
self._pin,
self.board.name,
self._initial,
)
api = self.board.api
await api.set_pin_mode_pwm_output(self._firmata_pin)
new_pin_state = round((self._initial * self._range) / 255) + self._min
await api.pwm_write(self._firmata_pin, new_pin_state)
self._state = self._initial
@property
def state(self) -> int:
"""Return PWM/analog state."""
return self._state
async def set_level(self, level: int) -> None:
"""Set PWM/analog output."""
_LOGGER.debug("Setting PWM/analog output on pin %s to %d", self._pin, level)
new_pin_state = round((level * self._range) / 255) + self._min
await self.board.api.pwm_write(self._firmata_pin, new_pin_state)
self._state = level
class FirmataBinaryDigitalInput(FirmataBoardPin):
"""Representation of a Firmata Digital Input Pin."""
@ -99,7 +148,7 @@ class FirmataBinaryDigitalInput(FirmataBoardPin):
async def start_pin(self, forward_callback: Callable[[], None]) -> None:
"""Get initial state and start reporting a pin."""
_LOGGER.debug(
"Starting reporting updates for input pin %s on board %s",
"Starting reporting updates for digital input pin %s on board %s",
self._pin,
self.board.name,
)
@ -133,7 +182,6 @@ class FirmataBinaryDigitalInput(FirmataBoardPin):
"""Return true if digital input is on."""
return self._state
@callback
async def latch_callback(self, data: list) -> None:
"""Update pin state on callback."""
if data[1] != self._firmata_pin:
@ -151,3 +199,65 @@ class FirmataBinaryDigitalInput(FirmataBoardPin):
return
self._state = new_state
self._forward_callback()
class FirmataAnalogInput(FirmataBoardPin):
"""Representation of a Firmata Analog Input Pin."""
def __init__(
self, board: FirmataBoard, pin: FirmataPinType, pin_mode: str, differential: int
):
"""Initialize the analog input pin."""
self._differential = differential
self._forward_callback = None
super().__init__(board, pin, pin_mode)
async def start_pin(self, forward_callback: Callable[[], None]) -> None:
"""Get initial state and start reporting a pin."""
_LOGGER.debug(
"Starting reporting updates for analog input pin %s on board %s",
self._pin,
self.board.name,
)
self._forward_callback = forward_callback
api = self.board.api
# Only PIN_MODE_ANALOG_INPUT mode is supported as sensor input
await api.set_pin_mode_analog_input(
self._analog_pin, self.latch_callback, self._differential
)
self._state = (await self.board.api.analog_read(self._analog_pin))[0]
self._forward_callback()
async def stop_pin(self) -> None:
"""Stop reporting analog input pin."""
_LOGGER.debug(
"Stopping reporting updates for analog input pin %s on board %s",
self._pin,
self.board.name,
)
api = self.board.api
await api.disable_analog_reporting(self._analog_pin)
@property
def state(self) -> int:
"""Return sensor state."""
return self._state
async def latch_callback(self, data: list) -> None:
"""Update pin state on callback."""
if data[1] != self._analog_pin:
return
_LOGGER.debug(
"Received latch %d for analog input pin %s on board %s",
data[2],
self._pin,
self.board.name,
)
new_state = data[2]
if self._state == new_state:
_LOGGER.debug("stopping")
return
self._state = new_state
self._forward_callback()

View file

@ -0,0 +1,59 @@
"""Support for Firmata sensor input."""
import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, CONF_PIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity
from .const import CONF_DIFFERENTIAL, CONF_PIN_MODE, DOMAIN
from .entity import FirmataPinEntity
from .pin import FirmataAnalogInput, FirmataPinUsedException
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities
) -> None:
"""Set up the Firmata sensors."""
new_entities = []
board = hass.data[DOMAIN][config_entry.entry_id]
for sensor in board.sensors:
pin = sensor[CONF_PIN]
pin_mode = sensor[CONF_PIN_MODE]
differential = sensor[CONF_DIFFERENTIAL]
api = FirmataAnalogInput(board, pin, pin_mode, differential)
try:
api.setup()
except FirmataPinUsedException:
_LOGGER.error(
"Could not setup sensor on pin %s since pin already in use",
sensor[CONF_PIN],
)
continue
name = sensor[CONF_NAME]
sensor_entity = FirmataSensor(api, config_entry, name, pin)
new_entities.append(sensor_entity)
if new_entities:
async_add_entities(new_entities)
class FirmataSensor(FirmataPinEntity, Entity):
"""Representation of a sensor on a Firmata board."""
async def async_added_to_hass(self) -> None:
"""Set up a sensor."""
await self._api.start_pin(self.async_write_ha_state)
async def async_will_remove_from_hass(self) -> None:
"""Stop reporting a sensor."""
await self._api.stop_pin()
@property
def state(self) -> int:
"""Return sensor state."""
return self._api.state

View file

@ -4,16 +4,10 @@ import logging
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME
from homeassistant.const import CONF_NAME, CONF_PIN
from homeassistant.core import HomeAssistant
from .const import (
CONF_INITIAL_STATE,
CONF_NEGATE_STATE,
CONF_PIN,
CONF_PIN_MODE,
DOMAIN,
)
from .const import CONF_INITIAL_STATE, CONF_NEGATE_STATE, CONF_PIN_MODE, DOMAIN
from .entity import FirmataPinEntity
from .pin import FirmataBinaryDigitalOutput, FirmataPinUsedException
@ -37,7 +31,7 @@ async def async_setup_entry(
api.setup()
except FirmataPinUsedException:
_LOGGER.error(
"Could not setup switch on pin %s since pin already in use.",
"Could not setup switch on pin %s since pin already in use",
switch[CONF_PIN],
)
continue
@ -55,7 +49,6 @@ class FirmataSwitch(FirmataPinEntity, SwitchEntity):
async def async_added_to_hass(self) -> None:
"""Set up a switch."""
await self._api.start_pin()
self.async_write_ha_state()
@property
def is_on(self) -> bool:
@ -64,12 +57,10 @@ class FirmataSwitch(FirmataPinEntity, SwitchEntity):
async def async_turn_on(self, **kwargs) -> None:
"""Turn on switch."""
_LOGGER.debug("Turning switch %s on", self._name)
await self._api.turn_on()
self.async_write_ha_state()
async def async_turn_off(self, **kwargs) -> None:
"""Turn off switch."""
_LOGGER.debug("Turning switch %s off", self._name)
await self._api.turn_off()
self.async_write_ha_state()

View file

@ -1470,7 +1470,7 @@ pylutron==0.2.5
pymailgunner==1.4
# homeassistant.components.firmata
pymata-express==1.13
pymata-express==1.19
# homeassistant.components.mediaroom
pymediaroom==0.6.4.1

View file

@ -713,7 +713,7 @@ pylutron-caseta==0.6.1
pymailgunner==1.4
# homeassistant.components.firmata
pymata-express==1.13
pymata-express==1.19
# homeassistant.components.melcloud
pymelcloud==2.5.2