Skip to content

Commit

Permalink
Dishwasher support (#26)
Browse files Browse the repository at this point in the history
* Dishwasher support

* Update README with dishwasher
  • Loading branch information
ofalvai authored Oct 13, 2021
1 parent 1891ed2 commit 60d3049
Show file tree
Hide file tree
Showing 8 changed files with 301 additions and 4 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@ This is still work-in-progress, it may not support every appliance type or featu


## Features
- Supported appliances: washing machine, tumble dryer, oven
- Supported appliances:
- washing machine
- tumble dryer
- oven
- dishwasher
- Uses the local API and its status endpoint
- Displays the machine status, wash cycle status, remaining time and some other attributes
- Creates various sensors, such as overall state and remaining time. Everything else is exposed as sensor attributes

## Installation

Expand Down
4 changes: 3 additions & 1 deletion custom_components/candy/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import backoff
from aiohttp import ClientSession

from .model import WashingMachineStatus, TumbleDryerStatus, OvenStatus
from .model import WashingMachineStatus, TumbleDryerStatus, DishwasherStatus, OvenStatus

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -44,6 +44,8 @@ async def status(self) -> Union[WashingMachineStatus, TumbleDryerStatus]:
status = WashingMachineStatus.from_json(resp_json["statusLavatrice"])
elif "statusForno" in resp_json:
status = OvenStatus.from_json(resp_json["statusForno"])
elif "statusDWash" in resp_json:
status = DishwasherStatus.from_json(resp_json["statusDWash"])
else:
raise Exception("Unable to detect machine type from API response", resp_json)

Expand Down
65 changes: 65 additions & 0 deletions custom_components/candy/client/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,71 @@ def from_json(cls, json):
)


class DishwasherState(Enum):
"""
Dishwashers have a single state combining the machine state and program state
"""

IDLE = 0
PRE_WASH = 1
WASH = 2
RINSE = 3
DRYING = 4
FINISHED = 5

def __str__(self):
if self == DishwasherState.IDLE:
return "Idle"
elif self == DishwasherState.PRE_WASH:
return "Pre-wash"
elif self == DishwasherState.WASH:
return "Wash"
elif self == DishwasherState.RINSE:
return "Rinse"
elif self == DishwasherState.DRYING:
return "Drying"
elif self == DishwasherState.FINISHED:
return "Finished"
else:
return "%s" % self


@dataclass
class DishwasherStatus:
machine_state: DishwasherState
program: str
remaining_minutes: int
door_open: bool
eco_mode: bool
remote_control: bool

@classmethod
def from_json(cls, json):
return cls(
machine_state=DishwasherState(int(json["StatoDWash"])),
program=DishwasherStatus.parse_program(json),
remaining_minutes=int(json["RemTime"]),
door_open=json["OpenDoor"] != "0",
eco_mode=json["Eco"] != "0",
remote_control=json["StatoWiFi"] == "1"
)

@staticmethod
def parse_program(json) -> str:
"""
Parse final program label, like P1, P1+, P1-
"""
program = json["Program"]
option = json["OpzProg"]
if option == "p":
return program + "+"
elif option == "m":
return program + "-"
else:
# Third OpzProg value is 0
return program


class OvenState(Enum):
IDLE = 0
HEATING = 1
Expand Down
3 changes: 3 additions & 0 deletions custom_components/candy/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@

UNIQUE_ID_OVEN = "{0}-oven"
UNIQUE_ID_OVEN_TEMP = "{0}-oven-temp"
UNIQUE_ID_DISHWASHER = "{0}-dishwasher"
UNIQUE_ID_DISHWASHER_REMAINING_TIME = "{0}-dishwasher_remaining_time"

DEVICE_NAME_WASHING_MACHINE = "Washing machine"
DEVICE_NAME_TUMBLE_DRYER = "Tumble dryer"
DEVICE_NAME_OVEN = "Oven"
DEVICE_NAME_DISHWASHER = "Dishwasher"

SUGGESTED_AREA_BATHROOM = "Bathroom"
SUGGESTED_AREA_KITCHEN = "Kitchen"
81 changes: 80 additions & 1 deletion custom_components/candy/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from homeassistant.helpers.typing import StateType
from .client import WashingMachineStatus
from .client.model import MachineState, TumbleDryerStatus, OvenStatus
from .client.model import MachineState, TumbleDryerStatus, OvenStatus, DishwasherStatus, DishwasherState
from .const import *
from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
Expand Down Expand Up @@ -36,6 +36,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
CandyOvenSensor(coordinator, config_id),
CandyOvenTempSensor(coordinator, config_id)
])
elif type(coordinator.data) is DishwasherStatus:
async_add_entities([
CandyDishwasherSensor(coordinator, config_id),
CandyDishwasherRemainingTimeSensor(coordinator, config_id)
])
else:
raise Exception(f"Unable to determine machine type: {coordinator.data}")

Expand Down Expand Up @@ -338,3 +343,77 @@ def unit_of_measurement(self) -> str:
@property
def icon(self) -> str:
return "mdi:thermometer"


class CandyDishwasherSensor(CandyBaseSensor):

def device_name(self) -> str:
return DEVICE_NAME_DISHWASHER

def suggested_area(self) -> str:
return SUGGESTED_AREA_KITCHEN

@property
def name(self) -> str:
return self.device_name()

@property
def unique_id(self) -> str:
return UNIQUE_ID_DISHWASHER.format(self.config_id)

@property
def state(self) -> StateType:
status: DishwasherStatus = self.coordinator.data
return str(status.machine_state)

@property
def icon(self) -> str:
return "mdi:glass-wine"

@property
def extra_state_attributes(self) -> Mapping[str, Any]:
status: DishwasherStatus = self.coordinator.data

attributes = {
"program": status.program,
"remaining_minutes": 0 if status.machine_state in
[DishwasherState.IDLE, DishwasherState.FINISHED] else status.remaining_minutes,
"remote_control": status.remote_control,
"door_open": status.door_open,
"eco_mode": status.eco_mode,
}

return attributes


class CandyDishwasherRemainingTimeSensor(CandyBaseSensor):

def device_name(self) -> str:
return DEVICE_NAME_DISHWASHER

def suggested_area(self) -> str:
return SUGGESTED_AREA_KITCHEN

@property
def name(self) -> str:
return "Dishwasher remaining time"

@property
def unique_id(self) -> str:
return UNIQUE_ID_DISHWASHER_REMAINING_TIME.format(self.config_id)

@property
def state(self) -> StateType:
status: DishwasherStatus = self.coordinator.data
if status.machine_state in [DishwasherState.IDLE, DishwasherState.FINISHED]:
return 0
else:
return status.remaining_minutes

@property
def unit_of_measurement(self) -> str:
return TIME_MINUTES

@property
def icon(self) -> str:
return "mdi:progress-clock"
21 changes: 21 additions & 0 deletions tests/fixtures/dishwasher/idle.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"statusDWash": {
"StatoWiFi": "1",
"StatoDWash": "0",
"CodiceErrore": "E0",
"StartStop": "0",
"Program": "P1",
"OpzProg": "p",
"DelayStart": "0",
"RemTime": "12",
"TreinUno": "0",
"Eco": "0",
"MetaCarico": "0",
"ExtraDry": "0",
"MissSalt": "0",
"MissRinse": "0",
"OpenDoor": "0",
"Reset": "0",
"FWver": "L1.12"
}
}
21 changes: 21 additions & 0 deletions tests/fixtures/dishwasher/wash.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"statusDWash": {
"StatoWiFi": "0",
"StatoDWash": "2",
"CodiceErrore": "E0",
"StartStop": "0",
"Program": "P2",
"OpzProg": "m",
"DelayStart": "0",
"RemTime": "68",
"TreinUno": "0",
"Eco": "1",
"MetaCarico": "0",
"ExtraDry": "0",
"MissSalt": "0",
"MissRinse": "0",
"OpenDoor": "0",
"Reset": "0",
"FWver": "L1.12"
}
}
102 changes: 102 additions & 0 deletions tests/test_sensor_dishwasher.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"""Tests for various sensors"""
from pytest_homeassistant_custom_component.common import MockConfigEntry, load_fixture
from pytest_homeassistant_custom_component.test_util.aiohttp import AiohttpClientMocker
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry, device_registry

from .common import init_integration


async def test_main_sensor_idle(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker):
await init_integration(hass, aioclient_mock, load_fixture("dishwasher/idle.json"))

state = hass.states.get("sensor.dishwasher")

assert state
assert state.state == "Idle"
assert state.attributes == {
"program": "P1+",
"remaining_minutes": 0,
"eco_mode": False,
"door_open": False,
"remote_control": True,
"friendly_name": "Dishwasher",
"icon": "mdi:glass-wine"
}


async def test_main_sensor_wash(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker):
await init_integration(hass, aioclient_mock, load_fixture("dishwasher/wash.json"))

state = hass.states.get("sensor.dishwasher")

assert state
assert state.state == "Wash"
assert state.attributes == {
"program": "P2-",
"remaining_minutes": 68,
"eco_mode": True,
"door_open": False,
"remote_control": False,
"friendly_name": "Dishwasher",
"icon": "mdi:glass-wine"
}


async def test_remaining_time_sensor_idle(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker):
await init_integration(hass, aioclient_mock, load_fixture("dishwasher/idle.json"))

state = hass.states.get("sensor.dishwasher_remaining_time")

assert state
assert state.state == "0"
assert state.attributes == {
"friendly_name": "Dishwasher remaining time",
"icon": "mdi:progress-clock",
"unit_of_measurement": "min",
}


async def test_remaining_time_sensor_wash(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker):
await init_integration(hass, aioclient_mock, load_fixture("dishwasher/wash.json"))

state = hass.states.get("sensor.dishwasher_remaining_time")

assert state
assert state.state == "68"
assert state.attributes == {
"friendly_name": "Dishwasher remaining time",
"icon": "mdi:progress-clock",
"unit_of_measurement": "min",
}


async def test_main_sensor_device_info(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker):
await init_integration(hass, aioclient_mock, load_fixture("dishwasher/idle.json"))

er = entity_registry.async_get(hass)
dr = device_registry.async_get(hass)
entry = er.async_get("sensor.dishwasher")
device = dr.async_get(entry.device_id)

assert device
assert device.manufacturer == "Candy"
assert device.name == "Dishwasher"
assert device.suggested_area == "Kitchen"


async def test_sensors_device_info(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker):
await init_integration(hass, aioclient_mock, load_fixture("dishwasher/idle.json"))

er = entity_registry.async_get(hass)
dr = device_registry.async_get(hass)

main_sensor = er.async_get("sensor.dishwasher")
time_sensor = er.async_get("sensor.dishwasher_remaining_time")

main_device = dr.async_get(main_sensor.device_id)
time_device = dr.async_get(time_sensor.device_id)

assert main_device
assert time_device
assert main_device == time_device

0 comments on commit 60d3049

Please sign in to comment.