Skip to content

Instantly share code, notes, and snippets.

@tarikjn
Last active September 17, 2024 02:44
Show Gist options
  • Save tarikjn/efe10b7b3910490f0d1ea66fd30a7f19 to your computer and use it in GitHub Desktop.
Save tarikjn/efe10b7b3910490f0d1ea66fd30a7f19 to your computer and use it in GitHub Desktop.
Friedrich A/C platform for home-assistant
"""
Support for Friedrich's Windows A/Cs with KWIFI module.
Only KWIFI modules bought after Jan 1, 2018.
configuration.yaml
climate:
- platform: friedrich
email: [email protected]
password: hunter2
Limitations:
- does not support heating features, it's easy to add support for, but need a tester to confirm codes for operation modes
- does not support pulling unit state or live MQTT communication, need documentation on Xively MQTT JWT auth
- Xively session expiration is not well tested
- 10 devices max, requires implementation of pagination for more
TODOs:
- need to add a way to customize attributes?
- fuzzy/case match operation modes et al.
"""
import logging
import json
import voluptuous as vol
from homeassistant.components.climate import (STATE_COOL, STATE_FAN_ONLY, ClimateDevice,
PLATFORM_SCHEMA,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE,
SUPPORT_FAN_MODE)
from homeassistant.const import (CONF_EMAIL, CONF_PASSWORD,
STATE_OFF,
TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH)
import homeassistant.helpers.config_validation as cv
import requests
_LOGGER = logging.getLogger(__name__)
SPEED_MAX = 'max'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_EMAIL): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
})
# see: https://developer.xively.com/docs/using-jwts for expiration etc.
def _xively_jwt_authenticate(email, password, account_id):
XIVELY_JWT_AUTH_URL = 'https://id.xively.com/api/v1/auth/login-user'
payload = {
'emailAddress': email,
'password': password,
'accountId': account_id,
}
# Using the json parameter in the request will change the Content-Type in the header to application/json.
req = requests.post(XIVELY_JWT_AUTH_URL, json=payload)
if req.status_code != requests.codes.ok:
_LOGGER.error("Error authenticating to Xively, code %d", req.status_code)
return None
return req.json()['jwt']
def _xively_list_devices(account_id, jwt):
FRIEDRICH_LIST_DEVICES = 'https://blueprint.xively.com/api/v1/devices'
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + jwt
}
req = requests.get(FRIEDRICH_LIST_DEVICES, headers=headers, params={'accountId': account_id})
if req.status_code != requests.codes.ok:
_LOGGER.error("Error listing Xively devices, code %d", req.status_code)
return None
return req.json()['devices']['results']
FRIEDRICH_XIVELY_ACCOUNT_ID = '5c474e0f-d563-4947-a3f1-b517138e9961'
FRIEDRICH_TEMP_TIMESERIE = 'https://timeseries.xively.com/api/v4/data/xi/blue/v1/{0}/d/{1}/temp/latest'
FRIEDRICH_PUBLISH_COMMAND = 'https://friedrich.broker.xively.com/messaging/publish/xi/blue/v1/{0}/d/{1}/command'
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the friedrich platform."""
# import requirements for connection
# Assign configuration variables.
email = config.get(CONF_EMAIL)
password = config.get(CONF_PASSWORD)
# Authenticate to Xively
jwt = _xively_jwt_authenticate(email, password, FRIEDRICH_XIVELY_ACCOUNT_ID)
if jwt is None:
_LOGGER.error("Could not connect to Friedrich account")
return
# Fetch devices
devices = _xively_list_devices(FRIEDRICH_XIVELY_ACCOUNT_ID, jwt)
if devices is None:
_LOGGER.error("Could not list Friedrich devices")
return
# Add devices
add_devices(FriedrichHVAC(device['name'], jwt, device['id']) for device in devices)
# pylint: disable=abstract-method
# pylint: disable=too-many-instance-attributes
class FriedrichHVAC(ClimateDevice):
"""Representation of a Friedrich HVAC unit."""
def __init__(self, name, jwt, device_id):
"""Initialize a Friedrich HVAC unit."""
# initialization attributes
self._name = name
self._jwt = jwt
self._device_id = device_id
# this attribute can be obtained from the Xively API
self._current_temperature = None
# these attributes cannot be obtained and have to be assumed (=start defaults)
self._target_temperature = 76
self._current_operation = STATE_OFF
self._current_fan_mode = SPEED_LOW
# fixed settings
self._operation_list = [STATE_OFF, STATE_COOL, STATE_FAN_ONLY]
self._fan_list = [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, SPEED_MAX]
self._unit_of_measurement = TEMP_FAHRENHEIT
self._target_temperature_step = 1
@property
def should_poll(self):
"""Polling needed for room temperature reading."""
return True
def update(self):
"""Read the room temperature from Fridrich HVAC unit."""
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer {0}'.format(self._jwt)
}
req = requests.get(FRIEDRICH_TEMP_TIMESERIE.format(FRIEDRICH_XIVELY_ACCOUNT_ID, self._device_id), headers=headers)
if req.status_code != requests.codes.ok:
_LOGGER.error("Error obtaining Fridrich HVAC unit ambiant temperature reading, code %d", req.status_code)
return
self._current_temperature = req.json()['numericValue']
def _command(self, target_temperature=None, operation_mode=None, fan_mode=None):
"""Device publish command method which simultaneously update all commands' state"""
COMMAND_OPERATION = {
STATE_OFF: 5,
STATE_COOL: 1,
STATE_FAN_ONLY: 3,
}
COMMAND_FAN_MODE = {
SPEED_LOW: 0,
SPEED_MEDIUM: 1,
SPEED_HIGH: 2,
SPEED_MAX: 3,
}
target_temperature = target_temperature or self._target_temperature
operation_mode = operation_mode or self._current_operation
fan_mode = fan_mode or self._current_fan_mode
headers = {
'Authorization': 'Bearer ' + self._jwt
}
payload = {
'csp': target_temperature,
'hsp': target_temperature - 8, # heat set point
'asp': target_temperature - 4, # auto mode set point
'mode': COMMAND_OPERATION[operation_mode],
'fanMode': 0, # 0=auto operation, 1=continuous operation
'fanSpeed': COMMAND_FAN_MODE[fan_mode], # the device also has an auto speed in Cool mode, unknown code
}
req = requests.post(FRIEDRICH_PUBLISH_COMMAND.format(FRIEDRICH_XIVELY_ACCOUNT_ID, self._device_id),
headers=headers,
json=payload)
if req.status_code != requests.codes.ok:
_LOGGER.error("Error publishing Fridrich HVAC unit command, code %d", req.status_code)
return None
return True
@property
def assumed_state(self):
return True # we are assuming command state attributes at all times as we do not get updates
# from the device except for ambiant room temperature
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | SUPPORT_FAN_MODE
@property
def name(self):
"""Return the name of the thermostat."""
return self._name
@property
def temperature_unit(self):
"""Return the unit of measurement."""
return self._unit_of_measurement
@property
def current_temperature(self):
"""Return the current temperature."""
return self._current_temperature
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._target_temperature
@property
def current_operation(self):
"""Return the current mode of the HVAC."""
return self._current_operation
@property
def current_fan_mode(self):
"""Return the fan setting."""
return self._current_fan_mode
@property
def operation_list(self):
"""List of available operation modes."""
return self._operation_list
@property
def fan_list(self):
"""Return the list of available fan modes."""
return self._fan_list
@property
def target_temperature_step(self):
"""Return the supported step of target temperature."""
return self._target_temperature_step
def set_operation_mode(self, operation_mode):
"""Set Fridrich HVAC operation mode (off, fan only, cool)."""
if self._command(operation_mode=operation_mode) is None:
_LOGGER.error("Could not set Friedrich HVAC unit operation mode")
return
self._current_operation = operation_mode
def turn_on(self):
self.set_operation_mode(STATE_COOL)
def turn_off(self):
self.set_operation_mode(STATE_OFF)
def set_fan_mode(self, fan_mode):
"""Set Friedrich HVAC fan speed mode (auto, low, medium, high, max)."""
if self._command(fan_mode=fan_mode) is None:
_LOGGER.error("Could not set Friedrich HVAC unit fan mode")
return
self._current_fan_mode = fan_mode
def set_temperature(self, **kwargs):
"""Set Friedrich HVAC cooling target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE)
if self._command(target_temperature=temperature) is None:
_LOGGER.error("Could not set Friedrich HVAC target temperature")
return
self._target_temperature = temperature
@ncd7
Copy link

ncd7 commented Aug 12, 2023

you mention the following about getting the device_id -- where do we find the Connect URL? I'm using the Friedrich Go app.
" device_id: 123 -- obtain this from the hash in the Fridrich Connect URL when you are viewing your appliance"
Thank you!

@Gnomey123
Copy link

Gnomey123 commented Sep 17, 2024

I'm a complete beginner with home assistant. I tried adding the files (using this fork as a reference.

However, I'm getting this error:
image

Here's my configuration.yaml:
image

Here's how my code is integrated:
image

Could anyone help me with this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment