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

Fixed new api #34

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
8 changes: 2 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,12 @@ Graphing water consumption is also nice. Note that the data returned by Grohe's
## Installation
- Ensure everything is set up and working in Grohe's Ondus app
- Copy this folder to `<config_dir>/custom_components/grohe_sense/`
- Go to https://idp2-apigw.cloud.grohe.com/v3/iot/oidc/login
- Bring up developer tools
- Log in, that'll try redirecting your browser with a 302 to an url starting with `ondus://idp2-apigw.cloud.grohe.com/v3/iot/oidc/token`, which an off-the-shelf Chrome will ignore
- You should see this failed redirect in your developer tools. Copy out the full URL and replace `ondus` with `https` and visit that URL (will likely only work once, and will expire, so don't be too slow).
- This gives you a json response. Save it and extract refresh_token from it (manually, or `jq .refresh_token < file.json`)

Put the following in your home assistant config (N.B., format has changed, this component is no longer configured as a sensor platform)
```
grohe_sense:
refresh_token: "YOUR_VERY_VERY_LONG_REFRESH_TOKEN"
username: "YOUR_GROHE_EMAIL"
password: "YOUR_GROHE_PASSWORD"
```

## Remarks on the "API"
Expand Down
57 changes: 50 additions & 7 deletions __init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import logging
import asyncio
import collections
import requests
from lxml import html
import json

import homeassistant.helpers.config_validation as cv
import voluptuous as vol
Expand All @@ -11,12 +14,14 @@

DOMAIN = 'grohe_sense'

CONF_REFRESH_TOKEN = 'refresh_token'
CONF_USERNAME = 'username'
CONF_PASSWORD = 'password'

CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema({
vol.Required(CONF_REFRESH_TOKEN): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
}),
},
extra=vol.ALLOW_EXTRA,
Expand All @@ -29,18 +34,53 @@

GroheDevice = collections.namedtuple('GroheDevice', ['locationId', 'roomId', 'applianceId', 'type', 'name'])

async def get_token(session, username, password):
try:
response = await session.get(BASE_URL + 'oidc/login')
except Exception as e:
_LOGGER.error('Get Refresh Token Exception %s', str(e))
else:
cookie = response.cookies
tree = html.fromstring(await response.text())

name = tree.xpath("//html/body/div/div/div/div/div/div/div/form")
action = name[0].action

payload = {
'username': username,
'password': password,
'Content-Type': 'application/x-www-form-urlencoded',
'origin': BASE_URL,
'referer': BASE_URL + 'oidc/login',
'X-Requested-With': 'XMLHttpRequest',
}
try:
response = await session.post(url = action, data = payload, cookies = cookie, allow_redirects=False)
except Exception as e:
_LOGGER.error('Get Refresh Token Action Exception %s', str(e))
else:
ondus_url = response.headers['Location'].replace('ondus', 'https')
try:
response = await session.get(url = ondus_url, cookies = cookie)
except Exception as e:
_LOGGER.error('Get Refresh Token Response Exception %s', str(e))
else:
response_json = json.loads(await response.text())

return response_json['refresh_token']

async def async_setup(hass, config):
_LOGGER.debug("Loading Grohe Sense")

await initialize_shared_objects(hass, config.get(DOMAIN).get(CONF_REFRESH_TOKEN))
await initialize_shared_objects(hass, config.get(DOMAIN).get(CONF_USERNAME), config.get(DOMAIN).get(CONF_PASSWORD))

await hass.helpers.discovery.async_load_platform('sensor', DOMAIN, {}, config)
await hass.helpers.discovery.async_load_platform('switch', DOMAIN, {}, config)
return True

async def initialize_shared_objects(hass, refresh_token):
async def initialize_shared_objects(hass, username, password):
session = aiohttp_client.async_get_clientsession(hass)
auth_session = OauthSession(session, refresh_token)
auth_session = OauthSession(session, username, password, await get_token(session, username, password))
devices = []

hass.data[DOMAIN] = { 'session': auth_session, 'devices': devices }
Expand All @@ -65,8 +105,10 @@ def __init__(self, error_code, reason):
self.reason = reason

class OauthSession:
def __init__(self, session, refresh_token):
def __init__(self, session, username, password, refresh_token):
self._session = session
self._username = username
self._password = password
self._refresh_token = refresh_token
self._access_token = None
self._fetching_new_token = None
Expand Down Expand Up @@ -126,7 +168,8 @@ async def _http_request(self, url, method='get', auth_token=None, headers={}, **
token = await auth_token.token(token)
else:
_LOGGER.error('Grohe sense refresh token is invalid (or expired), please update your configuration with a new refresh token')
raise OauthException(response.status, await response.text())
self._refresh_token = get_token(self._session, self._username, self._password)
token = await self.token(token)
else:
_LOGGER.debug('Request to %s returned status %d, %s', url, response.status, await response.text())
except OauthException as oe:
Expand Down
1 change: 1 addition & 0 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"domain": "grohe_sense",
"version": "0.0.1",
"name": "Grohe Sense",
"documentation": "https://github.com/gkreitz/homeassistant-grohe_sense",
"dependencies": [],
Expand Down
12 changes: 7 additions & 5 deletions sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
from homeassistant.const import (STATE_UNAVAILABLE, STATE_UNKNOWN, TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE, PERCENTAGE, DEVICE_CLASS_HUMIDITY, VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR, PRESSURE_MBAR, DEVICE_CLASS_PRESSURE, TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE, VOLUME_LITERS)
from homeassistant.const import (STATE_UNAVAILABLE, STATE_UNKNOWN, TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE, PERCENTAGE, DEVICE_CLASS_HUMIDITY, VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR, PRESSURE_BAR, DEVICE_CLASS_PRESSURE, TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE, VOLUME_LITERS)

from homeassistant.helpers import aiohttp_client

Expand All @@ -21,7 +21,7 @@
'temperature': SensorType(TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE, lambda x : x),
'humidity': SensorType(PERCENTAGE, DEVICE_CLASS_HUMIDITY, lambda x : x),
'flowrate': SensorType(VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR, None, lambda x : x * 3.6),
'pressure': SensorType(PRESSURE_MBAR, DEVICE_CLASS_PRESSURE, lambda x : x * 1000),
'pressure': SensorType(PRESSURE_BAR, DEVICE_CLASS_PRESSURE, lambda x : x * 1000),
'temperature_guard': SensorType(TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE, lambda x : x),
}

Expand Down Expand Up @@ -119,7 +119,8 @@ def parse_time(s):
return datetime.strptime(s, '%Y-%m-%dT%H:%M:%S.%f%z')

poll_from=self._poll_from.strftime('%Y-%m-%d')
measurements_response = await self._auth_session.get(BASE_URL + f'locations/{self._locationId}/rooms/{self._roomId}/appliances/{self._applianceId}/data?from={poll_from}')
measurements_response = await self._auth_session.get(BASE_URL + f'locations/{self._locationId}/rooms/{self._roomId}/appliances/{self._applianceId}/data/aggregated?from={poll_from}')
_LOGGER.debug('Data read: %s', measurements_response['data'])
if 'withdrawals' in measurements_response['data']:
withdrawals = measurements_response['data']['withdrawals']
_LOGGER.debug('Received %d withdrawals in response', len(withdrawals))
Expand All @@ -137,12 +138,13 @@ def parse_time(s):

if 'measurement' in measurements_response['data']:
measurements = measurements_response['data']['measurement']
measurements.sort(key = lambda x: x['timestamp'])
measurements.sort(key = lambda x: x['date'])
if len(measurements):
for key in SENSOR_TYPES_PER_UNIT[self._type]:
_LOGGER.debug('key: %s', key)
if key in measurements[-1]:
self._measurements[key] = measurements[-1][key]
self._poll_from = max(self._poll_from, parse_time(measurements[-1]['timestamp']))
self._poll_from = datetime.strptime(measurements[-1]['date'], '%Y-%m-%d')
else:
_LOGGER.info('Data response for appliance %s did not contain any measurements data', self._applianceId)

Expand Down