Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[wip] Adding dryer and washer support #32

Open
wants to merge 112 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
112 commits
Select commit Hold shift + click to select a range
ecf43c3
Update sensor.py
Oct 22, 2019
a903fba
Update sensor.py
Oct 22, 2019
f5bbece
Update sensor.py
Oct 22, 2019
822e896
Update README.md
Oct 22, 2019
924b884
Renamed sensor.py to dishwasher.py
Oct 28, 2019
38075f5
Added missing configuration settings
Oct 28, 2019
881e6fd
Refactoring (cleanup)
Oct 28, 2019
e23a45d
Cleaned up imports where-ever possible
Oct 28, 2019
af5afa0
Made imports generic and specific where possible
Oct 28, 2019
7c78ed9
re-adds the wideq import to functions
Oct 29, 2019
93c34f6
Import fixes
Oct 29, 2019
43919c8
Update climate.py
Oct 29, 2019
6e75f40
Update climate.py
Oct 29, 2019
a8146b8
Update __init__.py
Oct 29, 2019
4de6fc0
Update climate.py
Oct 29, 2019
1dc590f
Make device.name generic where possible
Oct 29, 2019
4403ff8
Reverted 'dishwasher' to 'sensor'
Oct 30, 2019
2ae1ad4
Language should be lowercase
Oct 30, 2019
5a27c8e
Updated casing of 'language' parameter
Oct 30, 2019
1662a44
Added Dutch region casing example.
Oct 30, 2019
0c16315
Update manifest.json
Oct 30, 2019
0a75573
Added python cache dir to gitinore
Oct 31, 2019
4cd1b4c
Extract LGDevice to seperate class
Nov 4, 2019
5cdbf92
Update sensor.py
Nov 4, 2019
ef0477a
Update climate.py
Nov 4, 2019
1e89c7a
Fix HA import errors
Nov 4, 2019
a9ac7a0
Restructure classes
Nov 4, 2019
f906842
Create basic LGDevices module
Nov 4, 2019
368fa7b
Update sensor.py
Nov 4, 2019
ba512d6
Update sensor.py
Nov 4, 2019
9d875ec
Fix region is not defined error
Nov 4, 2019
7b8b6d1
Update climate.py
Nov 4, 2019
b6f4ca7
Update sensor.py
Nov 4, 2019
bfe9140
Moved LGDevice to seperate class
Nov 4, 2019
bbc3176
Update LGDevice.py
Nov 4, 2019
e87cd57
Moved LGDishwasherDevice to its own class
Nov 5, 2019
8ee1f95
Cleanup sensor.py
Nov 5, 2019
5784ee0
Refactored MAX_RETRIES
Nov 5, 2019
7e29747
Refactoring climate devices
Nov 5, 2019
b9d0896
There shall be only one
Nov 5, 2019
f4e80a9
Revert "There shall be only one"
Nov 5, 2019
784479e
Added DeviceTypes folder
Nov 5, 2019
3d3c1d3
Update LGDishwasherDevice.py
gvdhoven Nov 6, 2019
9ff0b42
Create LGDryerDevice.py
gvdhoven Nov 6, 2019
ceebdc5
Update sensor.py
gvdhoven Nov 6, 2019
109e0a1
Update sensor.py
gvdhoven Nov 6, 2019
5797e63
Update LGDryerDevice.py
gvdhoven Nov 6, 2019
d026148
Update LGDryerDevice.py
gvdhoven Nov 6, 2019
9aa678f
Update LGDryerDevice.py
gvdhoven Nov 6, 2019
ca3fa57
Update LGDryerDevice.py
gvdhoven Nov 6, 2019
5c5dacc
Update LGDishwasherDevice.py
gvdhoven Nov 6, 2019
52c23b0
Update LGClimateDevice.py
gvdhoven Nov 6, 2019
562921e
Update LGDryerDevice.py
gvdhoven Nov 6, 2019
9044e78
Update LGDishwasherDevice.py
gvdhoven Nov 6, 2019
4a468a3
Update LGDryerDevice.py
gvdhoven Nov 6, 2019
bf04032
Update LGDryerDevice.py
gvdhoven Nov 6, 2019
0d5ec49
Preparing for adding Dehumidifier and washer
Nov 7, 2019
5310f1b
Update LGAcDevice.py
Nov 7, 2019
bce644d
Polling refactoring
Nov 7, 2019
d09c670
Update LGDryerDevice.py
Nov 7, 2019
3d86737
Update LGDryerDevice.py
gvdhoven Nov 7, 2019
3a57c79
Update LGDryerDevice.py
gvdhoven Nov 7, 2019
1c0d8f4
Update LGDevice.py
gvdhoven Nov 7, 2019
d52f3da
Update LGDryerDevice.py
gvdhoven Nov 7, 2019
ec1ee7d
Update LGDryerDevice.py
gvdhoven Nov 7, 2019
4986c42
Update LGDryerDevice.py
gvdhoven Nov 7, 2019
467165f
Update LGDryerDevice.py
gvdhoven Nov 7, 2019
41dbc22
Update LGDryerDevice.py
gvdhoven Nov 7, 2019
c92447e
Reduced dryer status back to bare minimum
Nov 11, 2019
4c02bec
Update todo.txt
Nov 11, 2019
9cda3d2
Create ___RC90U2_WW.txt
gvdhoven Nov 11, 2019
db52618
Update LGDryerDevice.py
gvdhoven Nov 12, 2019
1d8e1e8
Update LGDryerDevice.py
gvdhoven Nov 12, 2019
ae4a05e
Update LGDryerDevice.py
gvdhoven Nov 12, 2019
498ba1c
Update LGDryerDevice.py
gvdhoven Nov 12, 2019
60cfb94
Update LGDryerDevice.py
gvdhoven Nov 12, 2019
ff08e0b
Update LGDryerDevice.py
gvdhoven Nov 12, 2019
a3a34ff
Update LGDryerDevice.py
gvdhoven Nov 12, 2019
a054651
Update LGDryerDevice.py
gvdhoven Nov 12, 2019
a51f303
Update LGDryerDevice.py
gvdhoven Nov 12, 2019
4f2029d
Update LGDryerDevice.py
gvdhoven Nov 12, 2019
b497faa
Update LGDryerDevice.py
gvdhoven Nov 12, 2019
43c25e0
Update LGDryerDevice.py
gvdhoven Nov 12, 2019
97672b5
Update LGDryerDevice.py
gvdhoven Nov 12, 2019
fbc747f
Update LGDryerDevice.py
gvdhoven Nov 12, 2019
a78de29
Update LGDryerDevice.py
gvdhoven Nov 12, 2019
a4bb4ee
Update LGDryerDevice.py
gvdhoven Nov 12, 2019
3d97534
Update LGDryerDevice.py
gvdhoven Nov 12, 2019
4708a71
Update LGDryerDevice.py
gvdhoven Nov 12, 2019
bd76be5
Update LGDryerDevice.py
gvdhoven Nov 12, 2019
d95ca15
Update LGDryerDevice.py
gvdhoven Nov 12, 2019
dc07be8
Update LGDryerDevice.py
gvdhoven Nov 12, 2019
bd23610
Update LGDevice.py
gvdhoven Nov 12, 2019
5372975
Update LGDryerDevice.py
gvdhoven Nov 12, 2019
40e21eb
Update LGDryerDevice.py
gvdhoven Nov 12, 2019
77287ce
Update LGDryerDevice.py
gvdhoven Nov 12, 2019
02f5d14
Update LGDryerDevice.py
gvdhoven Nov 12, 2019
170f216
Update LGDryerDevice.py
gvdhoven Nov 12, 2019
416b5c1
Update LGDryerDevice.py
gvdhoven Nov 12, 2019
1eef46b
Update LGDryerDevice.py
gvdhoven Nov 12, 2019
154d784
Update LGDryerDevice.py
gvdhoven Nov 12, 2019
d7d015a
Update LGDryerDevice.py
gvdhoven Nov 12, 2019
cc42fc5
Update LGDryerDevice.py
gvdhoven Nov 12, 2019
c4d923a
Update LGDryerDevice.py
gvdhoven Nov 12, 2019
d6b581d
Update LGDryerDevice.py
gvdhoven Nov 12, 2019
eda4a52
Update LGDryerDevice.py
gvdhoven Nov 12, 2019
b4e04d5
Fix method comments
gvdhoven Nov 12, 2019
4bf799d
Update lookup_bit
gvdhoven Nov 12, 2019
6108938
Merge remote-tracking branch 'sampsyo/master' into PR-32 (Washer/drye…
Dec 10, 2019
e2975dd
Update LGDryerDevice.py
gvdhoven Jan 11, 2020
c49f07d
Merge pull request #1 from nonsleepr/PR-w-d-support
gvdhoven Apr 10, 2021
3267039
Update sensor.py
gvdhoven Apr 10, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.vscode/
__pycache__/
169 changes: 169 additions & 0 deletions DeviceTypes/LGAcDevice.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import wideq
import logging

"""General variables"""
REQUIREMENTS = ['wideq']
LOGGER = logging.getLogger(__name__)

"""Device specific imports"""
import time
from .LGDevice import LGDevice
from homeassistant.components import climate
from homeassistant.components.climate import ClimateDevice
from homeassistant.components.climate import const as c_const
from wideq import ac as wideq_ac

"""Implementation specific variables"""
MODES = {
'HEAT': c_const.HVAC_MODE_HEAT,
'COOL': c_const.HVAC_MODE_COOL,
'FAN': c_const.HVAC_MODE_FAN_ONLY,
'DRY': c_const.HVAC_MODE_DRY,
'ACO': c_const.HVAC_MODE_HEAT_COOL,
}
FAN_MODES = {
'LOW': c_const.FAN_LOW,
'LOW_MID': 'low-mid',
'MID': c_const.FAN_MEDIUM,
'MID_HIGH': 'mid-high',
'HIGH': c_const.FAN_HIGH,

'NATURE': 'nature',
'POWER': 'power',
}

TRANSIENT_EXP = 5.0 # Report set temperature for 5 seconds.
TEMP_MIN_F = 60 # Guessed from actual behavior: API reports are unreliable.
TEMP_MAX_F = 89
TEMP_MIN_C = 18 # Intervals read from the AC's remote control.
TEMP_MAX_C = 30

class LGAcDevice(LGDevice, ClimateDevice):
def __init__(self, client, max_retries, device, fahrenheit):
"""Initialize an LG Climate Device."""

# Call LGDevice constructor
super().__init__(client, max_retries, device, wideq_ac.ACDevice)

# Overwrite variables
self._name = "lg_ac_" + device.id
self._fahrenheit = fahrenheit

# Store a transient temperature when we've just set it. We also
# store the timestamp for when we set this value.
self._transient_temp = None
self._transient_time = None

@property
def temperature_unit(self):
if self._fahrenheit:
return const.TEMP_FAHRENHEIT
else:
return const.TEMP_CELSIUS

@property
def supported_features(self):
return (
c_const.SUPPORT_TARGET_TEMPERATURE |
c_const.SUPPORT_FAN_MODE
)

@property
def min_temp(self):
if self._fahrenheit:
return TEMP_MIN_F
else:
return TEMP_MIN_C

@property
def max_temp(self):
if self._fahrenheit:
return TEMP_MAX_F
else:
return TEMP_MAX_C

@property
def current_temperature(self):
if self._state:
if self._fahrenheit:
return self._state.temp_cur_f
else:
return self._state.temp_cur_c

@property
def target_temperature(self):
# Use the recently-set target temperature if it was set recently
# (within TRANSIENT_EXP seconds ago).
if self._transient_temp:
interval = time.time() - self._transient_time
if interval < TRANSIENT_EXP:
return self._transient_temp
else:
self._transient_temp = None

# Otherwise, actually use the device's state.
if self._state:
if self._fahrenheit:
return self._state.temp_cfg_f
else:
return self._state.temp_cfg_c

@property
def hvac_modes(self):
return list(MODES.values()) + [c_const.HVAC_MODE_OFF]

@property
def fan_modes(self):
return list(FAN_MODES.values())

@property
def hvac_mode(self):
if self._state:
if not self._state.is_on:
return c_const.HVAC_MODE_OFF
mode = self._state.mode
return MODES[mode.name]

@property
def fan_mode(self):
mode = self._state.fan_speed
return FAN_MODES[mode.name]

def set_hvac_mode(self, hvac_mode):
if hvac_mode == c_const.HVAC_MODE_OFF:
self._wideq_device.set_on(False)
return

# Some AC units must be powered on before setting the mode.
if not self._state.is_on:
self._wideq_device.set_on(True)

# Invert the modes mapping.
modes_inv = {v: k for k, v in MODES.items()}

mode = wideq_ac.ACMode[modes_inv[hvac_mode]]
LOGGER.info('Setting mode to %s...', mode)
self._wideq_device.set_mode(mode)
LOGGER.info('Mode set.')

def set_fan_mode(self, fan_mode):
# Invert the fan modes mapping.
fan_modes_inv = {v: k for k, v in FAN_MODES.items()}

mode = wideq_ac.ACFanSpeed[fan_modes_inv[fan_mode]]
LOGGER.info('Setting fan mode to %s', fan_mode)
self._wideq_device.set_fan_speed(mode)
LOGGER.info('Fan mode set.')

def set_temperature(self, **kwargs):
temperature = kwargs['temperature']
self._transient_temp = temperature
self._transient_time = time.time()

LOGGER.info('Setting temperature to %s...', temperature)
if self._fahrenheit:
self._wideq_device.set_fahrenheit(temperature)
else:
self._wideq_device.set_celsius(temperature)
LOGGER.info('Temperature set.')

82 changes: 82 additions & 0 deletions DeviceTypes/LGDevice.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import wideq
import logging

from homeassistant.helpers.entity import Entity

LOGGER = logging.getLogger(__name__)

class LGDevice(Entity):
def __init__(self, client, max_retries, device, wideq_constructor):
self._client = client
self._max_retries = max_retries
self._device = device

# Monitoring variables
self._status = None
self._name = "lg_device_" + device.id
self._failed_request_count = 0

# This constructor is called during platform creation. It must not
# involve any API calls that actually need the dishwasher to be
# connected, otherwise the device construction will fail and the entity
# will not get created. Specifically, calls that depend on dishwasher
# interaction should only happen in update(...), including the start of
# the monitor task.
self._wideq_device = wideq_constructor(client, device);

@property
def name(self):
return self._name

@property
def available(self):
return True

def _restart_monitor(self):
try:
self._wideq_device.monitor_start()
except wideq.NotConnectedError:
LOGGER.info('Device not available.')
self._status = None
except wideq.NotLoggedInError:
LOGGER.info('Session expired. Refreshing.')
self._client.refresh()

def update(self):
"""Poll for wideq device state updates."""

# This method is polled, so try to avoid sleeping in here. If an error
# occurs, it will naturally be retried on the next poll.
LOGGER.debug('Updating %s.', self._name)

# On initial construction, the dishwasher monitor task
# will not have been created. If so, start monitoring here.
if getattr(self._wideq_device, 'mon', None) is None:
self._restart_monitor()

try:
status = self._wideq_device.poll()
except wideq.NotConnectedError:
self._status = None
return
except wideq.NotLoggedInError:
LOGGER.info('Session expired. Refreshing.')
self._client.refresh()
self._restart_monitor()
return

if status:
LOGGER.debug('Status updated.')
self._status = status
self._failed_request_count = 0
return

LOGGER.debug('No status available yet.')
self._failed_request_count += 1

if self._failed_request_count >= self._max_retries:
# We tried several times but got no result. This might happen
# when the monitoring request gets into a bad state, so we
# restart the task.
self._restart_monitor()
self._failed_request_count = 0
118 changes: 118 additions & 0 deletions DeviceTypes/LGDishwasherDevice.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import wideq
import logging

"""General variables"""
REQUIREMENTS = ['wideq']
LOGGER = logging.getLogger(__name__)

"""Device specific imports"""
import datetime
from .LGDevice import LGDevice
from wideq import dishwasher as wideq_dishwasher

"""Device specific variables"""
ATTR_DW_STATE = 'state'
ATTR_DW_REMAINING_TIME = 'remaining_time'
ATTR_DW_REMAINING_TIME_IN_MINUTES = 'remaining_time_in_minutes'
ATTR_DW_INITIAL_TIME = 'initial_time'
ATTR_DW_INITIAL_TIME_IN_MINUTES = 'initial_time_in_minutes'
ATTR_DW_RESERVE_TIME = 'reserve_time'
ATTR_DW_RESERVE_TIME_IN_MINUTES = 'reserve_time_in_minutes'
ATTR_DW_COURSE = 'course'
ATTR_DW_ERROR = 'error'
ATTR_DW_DEVICE_TYPE = 'device_type'
KEY_DW_OFF = 'Off'
KEY_DW_DISCONNECTED = 'Disconnected'

class LGDishwasherDevice(LGDevice):
def __init__(self, client, max_retries, device):
"""Initialize an LG DishWasher Device."""

# Call LGDevice constructor
super().__init__(client, max_retries, device, wideq_dishwasher.DishWasherDevice)

# Overwrite variables
self._name = "lg_dishwasher_" + device.id

@property
def state_attributes(self):
"""Return the optional state attributes for the dishwasher."""
data = {}
data[ATTR_DW_REMAINING_TIME] = self.remaining_time
data[ATTR_DW_REMAINING_TIME_IN_MINUTES] = self.remaining_time_in_minutes
data[ATTR_DW_INITIAL_TIME] = self.initial_time
data[ATTR_DW_INITIAL_TIME_IN_MINUTES] = self.initial_time_in_minutes
data[ATTR_DW_RESERVE_TIME] = self.reserve_time
data[ATTR_DW_RESERVE_TIME_IN_MINUTES] = self.reserve_time_in_minutes
data[ATTR_DW_COURSE] = self.course
data[ATTR_DW_ERROR] = self.error

# For convenience, include the state as an attribute.
data[ATTR_DW_STATE] = self.state
return data

@property
def state(self):
if self._status:
# Process is a more refined string to use for state, if it's present,
# use it instead.
return self._status.readable_process or self._status.readable_state
return dishwasher.DISHWASHER_STATE_READABLE[dishwasher.DishWasherState.OFF.name]

@property
def remaining_time(self):
minutes = self.remaining_time_in_minutes if self._status else 0
return str(datetime.timedelta(minutes=minutes))[:-3]

@property
def remaining_time_in_minutes(self):
# The API (indefinitely) returns 1 minute remaining when a cycle is
# either in state off or complete, or process night-drying. Return 0
# minutes remaining in these instances, which is more reflective of
# reality.
if (self._status and
(self._status.process == wideq_dishwasher.DishWasherProcess.NIGHT_DRYING or
self._status.state == wideq_dishwasher.DishWasherState.OFF or
self._status.state == wideq_dishwasher.DishWasherState.COMPLETE)):
return 0
return self._status.remaining_time if self._status else 0

@property
def initial_time(self):
minutes = self.initial_time_in_minutes if self._status else 0
return str(datetime.timedelta(minutes=minutes))[:-3]

@property
def initial_time_in_minutes(self):
# When in state OFF, the dishwasher still returns the initial program
# length of the previously ran cycle. Instead, return 0 which is more
# reflective of the dishwasher being off.
if (self._status and
self._status.state == wideq_dishwasher.DishWasherState.OFF):
return 0
return self._status.initial_time if self._status else 0

@property
def reserve_time(self):
minutes = self.reserve_time_in_minutes if self._status else 0
return str(datetime.timedelta(minutes=minutes))[:-3]

@property
def reserve_time_in_minutes(self):
return self._status.reserve_time if self._status else 0

@property
def course(self):
if self._status:
if self._status.smart_course != KEY_DW_OFF:
return self._status.smart_course
else:
return self._status.course
return KEY_DW_OFF

@property
def error(self):
if self._status:
return self._status.error
return KEY_DW_DISCONNECTED