From f5eaba2320a96dcb8eed2908f22f585289d5f9bc Mon Sep 17 00:00:00 2001 From: Stephen Papierski Date: Sat, 13 Feb 2021 00:10:56 -0700 Subject: [PATCH 1/5] Properties now return info from memory Per HA docs, "Properties should always only return information from memory and not do I/O (like network requests)." --- custom_components/traeger/climate.py | 118 ++++++++++++++++----------- custom_components/traeger/const.py | 9 ++ custom_components/traeger/sensor.py | 85 +++++++++++++------ 3 files changed, 138 insertions(+), 74 deletions(-) diff --git a/custom_components/traeger/climate.py b/custom_components/traeger/climate.py index f4868de..e4ea733 100644 --- a/custom_components/traeger/climate.py +++ b/custom_components/traeger/climate.py @@ -1,4 +1,4 @@ -"""Binary sensor platform for integration_blueprint.""" +"""Climate platform for Traeger grills""" import logging @@ -20,6 +20,14 @@ from .const import ( DEFAULT_NAME, DOMAIN, + GRILL_MODE_OFFLINE, + GRILL_MODE_COOL_DOWN, + GRILL_MODE_CUSTOM_COOK, + GRILL_MODE_MANUAL_COOK, + GRILL_MODE_PREHEATING, + GRILL_MODE_IGNITING, + GRILL_MODE_IDLE, + GRILL_MODE_SLEEPING, ) from .entity import IntegrationBlueprintEntity @@ -31,29 +39,40 @@ async def async_setup_entry(hass, entry, async_add_devices): client = hass.data[DOMAIN][entry.entry_id] grills = client.get_grills() for grill in grills: - async_add_devices( - [IntegrationBlueprintBinarySensor(client, grill["thingName"])] - ) + async_add_devices([TraegerClimateEntity(client, grill["thingName"])]) -class IntegrationBlueprintBinarySensor(ClimateEntity, IntegrationBlueprintEntity): - """integration_blueprint binary_sensor class.""" - - def grill_update(self): - self.schedule_update_ha_state() +class TraegerClimateEntity(ClimateEntity, IntegrationBlueprintEntity): + """Climate entity for Traeger grills""" def __init__(self, client, grill_id): self.grill_id = grill_id self.client = client + self.grill_details = None + self.grill_state = None + self.grill_units = None + self.grill_limits = None + + # Tell the Traeger client to call grill_update() when it gets an update self.client.set_callback_for_grill(self.grill_id, self.grill_update) + def grill_update(self): + """This gets called when the grill has an update. Update state variable""" + self.grill_details = self.client.get_details_for_device(self.grill_id) + self.grill_state = self.client.get_state_for_device(self.grill_id) + self.grill_units = self.client.get_units_for_device(self.grill_id) + self.grill_limits = self.client.get_limits_for_device(self.grill_id) + + # Tell HA we have an update + self.schedule_update_ha_state() + + # Generic Properties @property def name(self): - """Return the name of the binary_sensor.""" - state = self.client.get_details_for_device(self.grill_id) - if state is None: + """Return the name of the grill""" + if self.grill_details is None: return f"{self.grill_id}" - return state["friendlyName"] + return self.grill_details["friendlyName"] @property def unique_id(self): @@ -63,68 +82,65 @@ def unique_id(self): def icon(self): return "mdi:grill" + # Climate Properties + @property + def temperature_unit(self): + if self.grill_units == TEMP_CELSIUS: + return TEMP_CELSIUS + else: + return TEMP_FAHRENHEIT + @property def current_temperature(self): - state = self.client.get_state_for_device(self.grill_id) - if state is None: + if self.grill_state is None: return 0 - return state["grill"] + return self.grill_state["grill"] @property def target_temperature(self): - state = self.client.get_state_for_device(self.grill_id) - if state is None: + if self.grill_state is None: return 0 - return state["set"] - - @property - def min_temp(self): - # this was the min the traeger app would let me set - if self.client.get_units_for_device(self.grill_id) == TEMP_CELSIUS: - return 75 - else: - return 165 + return self.grill_state["set"] @property def max_temp(self): - limits = self.client.get_limits_for_device(self.grill_id) - if limits is None: + if self.grill_limits is None: return self.min_temp - return limits["max_grill_temp"] + return self.grill_limits["max_grill_temp"] @property - def supported_features(self): - """Return the list of supported features for the grill""" - return SUPPORT_TARGET_TEMPERATURE - - @property - def temperature_unit(self): - if self.client.get_units_for_device(self.grill_id) == TEMP_CELSIUS: - return TEMP_CELSIUS + def min_temp(self): + # this was the min the traeger app would let me set + if self.grill_units == TEMP_CELSIUS: + return 75 else: - return TEMP_FAHRENHEIT + return 165 @property def hvac_mode(self): """Return hvac operation ie. heat, cool mode. Need to be one of HVAC_MODE_*. """ - state = self.client.get_state_for_device(self.grill_id) - if state is None: + if self.grill_state is None: return HVAC_MODE_OFF - elif state["system_status"] == 8: # Cool Down + + state = self.grill_state["system_status"] + + if state == GRILL_MODE_COOL_DOWN: # Cool Down return HVAC_MODE_COOL - elif state["system_status"] == 7: # Custom Cook + elif state == GRILL_MODE_CUSTOM_COOK: # Custom Cook return HVAC_MODE_HEAT - elif state["system_status"] == 6: # Manual Cook + elif state == GRILL_MODE_MANUAL_COOK: # Manual Cook return HVAC_MODE_HEAT - elif state["system_status"] == 5: # Preheating + elif state == GRILL_MODE_PREHEATING: # Preheating return HVAC_MODE_HEAT - elif state["system_status"] == 4: # Igniting + elif state == GRILL_MODE_IGNITING: # Igniting return HVAC_MODE_HEAT - elif state["system_status"] == 3: # Idle (Power switch on, screen on) + elif state == GRILL_MODE_IDLE: # Idle (Power switch on, screen on) + return HVAC_MODE_OFF + elif state == GRILL_MODE_SLEEPING: # Sleeping (Power switch on, screen off) return HVAC_MODE_OFF - elif state["system_status"] == 2: # Sleeping (Power switch on, screen off) + elif state == GRILL_MODE_OFFLINE: # Offline return HVAC_MODE_OFF else: return HVAC_MODE_OFF @@ -136,6 +152,12 @@ def hvac_modes(self): """ return (HVAC_MODE_HEAT, HVAC_MODE_OFF, HVAC_MODE_COOL) + @property + def supported_features(self): + """Return the list of supported features for the grill""" + return SUPPORT_TARGET_TEMPERATURE + + # Climate Methods async def async_set_temperature(self, **kwargs): """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) diff --git a/custom_components/traeger/const.py b/custom_components/traeger/const.py index 7ff0743..99ac940 100644 --- a/custom_components/traeger/const.py +++ b/custom_components/traeger/const.py @@ -23,6 +23,15 @@ # Defaults DEFAULT_NAME = DOMAIN +# Grill Modes +GRILL_MODE_OFFLINE = 99 +GRILL_MODE_COOL_DOWN = 8 +GRILL_MODE_CUSTOM_COOK = 7 +GRILL_MODE_MANUAL_COOK = 6 +GRILL_MODE_PREHEATING = 5 +GRILL_MODE_IGNITING = 4 +GRILL_MODE_IDLE = 3 +GRILL_MODE_SLEEPING = 2 STARTUP_MESSAGE = f""" ------------------------------------------------------------------- diff --git a/custom_components/traeger/sensor.py b/custom_components/traeger/sensor.py index 8fd0c44..fa34085 100644 --- a/custom_components/traeger/sensor.py +++ b/custom_components/traeger/sensor.py @@ -1,4 +1,6 @@ """Sensor platform for Traeger.""" +import logging + from homeassistant.helpers.entity import Entity from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT @@ -22,81 +24,112 @@ async def async_setup_entry(hass, entry, async_add_devices): return for accessory in state["acc"]: if accessory["type"] == "probe": - async_add_devices([AccessoryTemperatureSensor( - client, grill_id, accessory["uuid"])]) + async_add_devices( + [AccessoryTemperatureSensor(client, grill_id, accessory["uuid"])] + ) - async_add_devices( - [ValueTemperature(client, grill["thingName"], "grill")]) - async_add_devices( - [ValueTemperature(client, grill["thingName"], "ambient")]) + async_add_devices([ValueTemperature(client, grill["thingName"], "grill")]) + async_add_devices([ValueTemperature(client, grill["thingName"], "ambient")]) + async_add_devices([PelletSensor(client, grill["thingName"], "pellet_level")]) class ValueTemperature(IntegrationBlueprintEntity): """Traeger Temperature Value class.""" - def grill_update(self): - self.schedule_update_ha_state() - def __init__(self, client, grill_id, value): self.grill_id = grill_id self.client = client self.value = value + self.grill_state = None + self.grill_units = None + + # Tell the Traeger client to call grill_update() when it gets an update self.client.set_callback_for_grill(self.grill_id, self.grill_update) + def grill_update(self): + self.grill_state = self.client.get_state_for_device(self.grill_id) + self.grill_units = self.client.get_units_for_device(self.grill_id) + + # Tell HA we have an update + self.schedule_update_ha_state() + + # Generic Properties @property def name(self): - """Return the name of the binary_sensor.""" + """Return the name of the sensor.""" return f"{self.value.capitalize()} Temperature" @property def unique_id(self): return f"{self.grill_id}-{self.value}" + @property + def icon(self): + return "mdi:thermometer" + + # Sensor Properties @property def state(self): - state = self.client.get_state_for_device(self.grill_id) - if state is None: + if self.grill_state is None: return 0 - return state[self.value] + return self.grill_state[self.value] @property def unit_of_measurement(self): - return self.client.get_units_for_device(self.grill_id) + return self.grill_units class AccessoryTemperatureSensor(IntegrationBlueprintEntity): """Traeger Temperature Accessory class.""" - def grill_update(self): - self.schedule_update_ha_state() - def __init__(self, client, grill_id, sensor_id): self.grill_id = grill_id self.client = client self.sensor_id = sensor_id + self.grill_state = None + self.grill_units = None + self.grill_details = None + self.grill_accessory = None + + # Tell the Traeger client to call grill_update() when it gets an update self.client.set_callback_for_grill(self.grill_id, self.grill_update) + def grill_update(self): + self.grill_state = self.client.get_state_for_device(self.grill_id) + self.grill_units = self.client.get_units_for_device(self.grill_id) + self.grill_details = self.client.get_details_for_device(self.grill_id) + self.grill_accessory = self.client.get_details_for_accessory( + self.grill_id, self.sensor_id + ) + + # Tell HA we have an update + self.schedule_update_ha_state() + + # Generic Properties @property def name(self): - """Return the name of the binary_sensor.""" - details = self.client.get_details_for_device(self.grill_id) - name = details["friendlyName"] - if details is None: + """Return the name of the sensor.""" + if self.grill_details is None: return f"{self.grill_id}-{self.sensor_id}" + + name = self.grill_details["friendlyName"] return f"{name}-{self.sensor_id}" @property def unique_id(self): return f"{self.grill_id}-{self.sensor_id}" + @property + def icon(self): + return "mdi:thermometer" + + # Sensor Properties @property def state(self): - accessory = self.client.get_details_for_accessory( - self.grill_id, self.sensor_id) - if accessory is None: + if self.grill_accessory is None: return 0 - return accessory["probe"]["get_temp"] + return self.grill_accessory["probe"]["get_temp"] @property def unit_of_measurement(self): - return self.client.get_units_for_device(self.grill_id) + return self.grill_units From 24cc1fb9ed874ce48768a2c448adccecc46592d0 Mon Sep 17 00:00:00 2001 From: Stephen Papierski Date: Sat, 13 Feb 2021 00:14:10 -0700 Subject: [PATCH 2/5] Climate and sensor report as not available when grill is offline --- custom_components/traeger/climate.py | 8 ++++++++ custom_components/traeger/sensor.py | 16 ++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/custom_components/traeger/climate.py b/custom_components/traeger/climate.py index e4ea733..aa512e0 100644 --- a/custom_components/traeger/climate.py +++ b/custom_components/traeger/climate.py @@ -67,6 +67,14 @@ def grill_update(self): self.schedule_update_ha_state() # Generic Properties + @property + def available(self): + """Reports unavailable when the grill is powered off""" + if self.grill_state is None: + return False + else: + return True if self.grill_state["connected"] == True else False + @property def name(self): """Return the name of the grill""" diff --git a/custom_components/traeger/sensor.py b/custom_components/traeger/sensor.py index fa34085..b6ebe48 100644 --- a/custom_components/traeger/sensor.py +++ b/custom_components/traeger/sensor.py @@ -54,6 +54,14 @@ def grill_update(self): self.schedule_update_ha_state() # Generic Properties + @property + def available(self): + """Reports unavailable when the grill is powered off""" + if self.grill_state is None: + return False + else: + return True if self.grill_state["connected"] == True else False + @property def name(self): """Return the name of the sensor.""" @@ -106,6 +114,14 @@ def grill_update(self): self.schedule_update_ha_state() # Generic Properties + @property + def available(self): + """Reports unavailable when the grill is powered off""" + if self.grill_state is None: + return False + else: + return True if self.grill_state["connected"] == True else False + @property def name(self): """Return the name of the sensor.""" From e279a35ce5d13c1f9d1a32013e8b23503d4c621c Mon Sep 17 00:00:00 2001 From: Stephen Papierski Date: Sat, 13 Feb 2021 00:15:44 -0700 Subject: [PATCH 3/5] Shutdown grill when hvac_mode set to HVAC_MODE_OFF or HVAC_MODE_COOL --- custom_components/traeger/climate.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/custom_components/traeger/climate.py b/custom_components/traeger/climate.py index aa512e0..e776d22 100644 --- a/custom_components/traeger/climate.py +++ b/custom_components/traeger/climate.py @@ -170,3 +170,8 @@ async def async_set_temperature(self, **kwargs): """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) self.client.set_temperature(self.grill_id, temperature) + + def set_hvac_mode(self, hvac_mode): + """Start grill shutdown sequence""" + if hvac_mode == HVAC_MODE_OFF or hvac_mode == HVAC_MODE_COOL: + self.client.shutdown_grill(self.grill_id) From bf66e6f664f495d4d71cd4c70a09cdedc0d0e657 Mon Sep 17 00:00:00 2001 From: Stephen Papierski Date: Sat, 13 Feb 2021 00:18:06 -0700 Subject: [PATCH 4/5] Updated translations to align with climate and sensor entities --- custom_components/traeger/translations/en.json | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/custom_components/traeger/translations/en.json b/custom_components/traeger/translations/en.json index a21b07b..2b56c4c 100644 --- a/custom_components/traeger/translations/en.json +++ b/custom_components/traeger/translations/en.json @@ -22,11 +22,10 @@ "step": { "user": { "data": { - "binary_sensor": "Binary sensor enabled", - "sensor": "Sensor enabled", - "switch": "Switch enabled" + "sensor": "Sensors enabled", + "climate": "Climate entity enabled" } } } } -} \ No newline at end of file +} From e40a179de621a4eb0ccea7e3cb8ec571f554c608 Mon Sep 17 00:00:00 2001 From: Stephen Papierski Date: Sat, 13 Feb 2021 00:18:56 -0700 Subject: [PATCH 5/5] Added a pellet level sensor --- custom_components/traeger/sensor.py | 54 +++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/custom_components/traeger/sensor.py b/custom_components/traeger/sensor.py index b6ebe48..5cf62de 100644 --- a/custom_components/traeger/sensor.py +++ b/custom_components/traeger/sensor.py @@ -149,3 +149,57 @@ def state(self): @property def unit_of_measurement(self): return self.grill_units + + +class PelletSensor(IntegrationBlueprintEntity): + """Traeger Pellet Sensor class.""" + + def __init__(self, client, grill_id, value): + self.grill_id = grill_id + self.client = client + self.value = value + self.grill_state = None + self.grill_units = None + + # Tell the Traeger client to call grill_update() when it gets an update + self.client.set_callback_for_grill(self.grill_id, self.grill_update) + + def grill_update(self): + self.grill_state = self.client.get_state_for_device(self.grill_id) + self.grill_units = self.client.get_units_for_device(self.grill_id) + + # Tell HA we have an update + self.schedule_update_ha_state() + + # Generic Properties + # @property + # def available(self): + # """Reports unavailable when the grill is powered off""" + # if self.grill_state is None: + # return False + # else: + # return True if self.grill_state["connected"] == True else False + + @property + def name(self): + """Return the name of the sensor.""" + return f"{self.value.replace('_',' ').title()}" + + @property + def unique_id(self): + return f"{self.grill_id}-{self.value}" + + @property + def icon(self): + return "mdi:gauge" + + # Sensor Properties + @property + def state(self): + if self.grill_state is None: + return 0 + return self.grill_state[self.value] + + @property + def unit_of_measurement(self): + return "%"