Skip to content

Instantly share code, notes, and snippets.

@sdague
Created January 6, 2018 12:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sdague/2a54490f8b0d70f5d63d6c36cd06edeb to your computer and use it in GitHub Desktop.
Save sdague/2a54490f8b0d70f5d63d6c36cd06edeb to your computer and use it in GitHub Desktop.
"""
Support for Waterfurnace
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.waterfurnace/
"""
import asyncio
from datetime import timedelta
import json
import logging
import re
import requests
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.core import callback
from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, TEMP_FAHRENHEIT
)
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
import websockets
_LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5)
SCAN_INTERVAL = timedelta(seconds=10)
USER_AGENT = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/42.0.2311.90 Safari/537.36")
WF_LOGIN_URL = 'https://symphony.mywaterfurnace.com/account/login'
class WFSensorConfig(object):
"""Water Furnace Sensor configuration."""
def __init__(self, friendly_name, field, icon="mdi:guage",
unit_of_measurement=None):
self.friendly_name = friendly_name
self.field = field
self.icon = icon
self.unit_of_measurement = unit_of_measurement
SENSORS = [
WFSensorConfig("Furnace Mode", "mode"),
WFSensorConfig("Total Power", "totalunitpower", "mdi:flash", "W"),
WFSensorConfig("Active Setpoint", "tstatactivesetpoint", "mdi:thermometer", TEMP_FAHRENHEIT),
WFSensorConfig("Leaving Air", "leavingairtemp", "mdi:thermometer", TEMP_FAHRENHEIT),
WFSensorConfig("Room Temp", "tstatroomtemp", "mdi:thermometer", TEMP_FAHRENHEIT),
WFSensorConfig("Loop Temp", "enteringwatertemp", "mdi:thermometer", TEMP_FAHRENHEIT),
WFSensorConfig("Humidity Set Point", "tstathumidsetpoint", "mdi:water-percent", "%"),
WFSensorConfig("Humidity", "tstatrelativehumidity", "mdi:water-percent", "%"),
]
@asyncio.coroutine
def async_setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the WUnderground sensor."""
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
unit = config.get("unit")
rest = WaterFurnaceData(
hass, username, password, unit)
yield from rest.login()
sensors = []
for config in SENSORS:
sensors.append(WaterFurnaceSensor(rest, config))
yield from rest.async_update()
add_devices(sensors)
return True
FURNACE_MODE = (
'Standby',
'Fan Only',
'Cooling 1',
'Cooling 2',
'Reheat',
'Heating 1',
'Heating 2',
'E-Heat',
'Aux Heat',
'Lockout')
class FurnaceReading(object):
def __init__(self, data={}):
self.zone = data.get('zone', 0)
self.err = data.get('err', '')
self.awlid = data.get('awlid', '')
self.tid = data.get('tid', 0)
# power (Watts)
self.compressorpower = data.get('compressorpower')
self.fanpower = data.get('fanpower')
self.auxpower = data.get('auxpower')
self.looppumppower = data.get('looppumppower')
self.totalunitpower = data.get('totalunitpower')
# modes (0 - 10)
self.modeofoperation = data.get('modeofoperation')
# fan speed (0 - 10)
self.airflowcurrentspeed = data.get('airflowcurrentspeed')
# humidity (%)
self.tstatdehumidsetpoint = data.get('tstatdehumidsetpoint')
self.tstathumidsetpoint = data.get('tstathumidsetpoint')
self.tstatrelativehumidity = data.get('tstatrelativehumidity')
# temps (degrees F)
self.leavingairtemp = data.get('leavingairtemp')
self.tstatroomtemp = data.get('tstatroomtemp')
self.enteringwatertemp = data.get('enteringwatertemp')
# setpoints (degrees F)
self.tstatheatingsetpoint = data.get('tstatheatingsetpoint')
self.tstatcoolingsetpoint = data.get('tstatcoolingsetpoint')
self.tstatactivesetpoint = data.get('tstatactivesetpoint')
@property
def mode(self):
return FURNACE_MODE[self.modeofoperation]
def __str__(self):
return ("<FurnaceReading power=%d, mode=%s, looptemp=%.1f, "
"airtemp=%.1f, roomtemp=%.1f, setpoint=%d>" % (
self.totalunitpower,
self.mode,
self.enteringwatertemp,
self.leavingairtemp,
self.tstatroomtemp,
self.tstatactivesetpoint))
class WaterFurnaceSensor(Entity):
"""Implementing the WUnderground sensor."""
def __init__(self, rest, config):
"""Initialize the sensor."""
self.rest = rest
self._name = config.friendly_name
self._attr = config.field
self._state = None
self._icon = config.icon
self._entity_picture = None
self._attributes = {}
self._unit_of_measurement = config.unit_of_measurement
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def entity_id(self):
"""Return the entity id."""
return "sensor.wf_" + self._attr
@property
def state(self):
"""Return the state of the sensor."""
return self._state
@property
def device_state_attributes(self):
"""Return the state attributes."""
return self._attributes
@property
def icon(self):
"""Return icon."""
return self._icon
@property
def entity_picture(self):
"""Return the entity picture."""
return self._entity_picture
@property
def unit_of_measurement(self):
"""Return the units of measurement."""
return self._unit_of_measurement
@asyncio.coroutine
def async_update(self):
"""Update current conditions."""
try:
yield from self.rest.async_update()
except TypeError:
# If this returns None due to time blocking, it's fine,
# just move on.
pass
if not self.rest.data:
# no data, return
return
self._state = getattr(self.rest.data, self._attr, None)
class WaterFurnaceData(object):
def __init__(self, hass, user, passwd, unit):
"""Initialize the data object."""
self.hass = hass
self._user = user
self._passwd = passwd
self._unit = unit
self.data = None
self.session_id = None
def _get_session_id(self):
data = dict(emailaddress=self._user, password=self._passwd, op="login")
headers = {"user-agent": USER_AGENT}
res = requests.post(WF_LOGIN_URL, data=data, headers=headers,
allow_redirects=False)
self.sessionid = res.cookies["sessionid"]
@asyncio.coroutine
def _login_ws(self):
self.ws = yield from websockets.connect(
"wss://awlclientproxy.mywaterfurnace.com/")
login = {"cmd": "login", "tid": 2, "source": "consumer dashboard",
"sessionid": self.sessionid}
yield from self.ws.send(json.dumps(login))
data = yield from self.ws.recv()
@asyncio.coroutine
def login(self):
yield from self.hass.loop.run_in_executor(None, self._get_session_id)
yield from self._login_ws()
@asyncio.coroutine
def read(self):
req = {
"cmd": "read",
"tid": 3,
"awlid": self._unit,
"zone": 0,
"rlist": ["compressorpower","fanpower","auxpower","looppumppower",
"totalunitpower","AWLABCType","ModeOfOperation",
"ActualCompressorSpeed","AirflowCurrentSpeed",
"AuroraOutputEH1","AuroraOutputEH2","AuroraOutputCC",
"AuroraOutputCC2","TStatDehumidSetpoint", "TStatHumidSetpoint",
"TStatRelativeHumidity","LeavingAirTemp","TStatRoomTemp",
"EnteringWaterTemp","AOCEnteringWaterTemp",
"lockoutstatus","lastfault","lastlockout",
"humidity_offset_settings","humidity","outdoorair",
"homeautomationalarm1","homeautomationalarm2","roomtemp",
"activesettings","TStatActiveSetpoint","TStatMode",
"TStatHeatingSetpoint","TStatCoolingSetpoint",
"AWLTStatType"],
"source":"consumer dashboard"}
yield from self.ws.send(json.dumps(req))
data = yield from self.ws.recv()
datadecoded = json.loads(data)
self.data = FurnaceReading(datadecoded)
@Throttle(MIN_TIME_BETWEEN_UPDATES)
@asyncio.coroutine
def async_update(self):
"""Get the latest data from Symphony websocket."""
try:
# self.login()
yield from self.read()
except ConnectionError as err:
yield from self.login()
_LOGGER.error("Lost our connection, trying again")
self.data = None
except requests.RequestException as err:
yield from self.login()
_LOGGER.error("Error fetching Waterfurnace data: %s", repr(err))
self.data = None
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment