Skip to content

Commit

Permalink
Working service for minimum charge limit and clima temp. Switch for d…
Browse files Browse the repository at this point in the history
…eparture schedules 1..3 on/off.
  • Loading branch information
milkboy committed Feb 23, 2022
1 parent 7ab92e6 commit 1a8885a
Show file tree
Hide file tree
Showing 7 changed files with 269 additions and 189 deletions.
2 changes: 1 addition & 1 deletion tests/integration_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ async def test_set_timer():
connection = vw_connection.Connection(session, username, password)
await connection.doLogin()
data = TimerData({}, {})
await connection.setSchedule(vin=vin, data=data)
await connection.setTimersAndProfiles(vin=vin, data=data)

assert 1 == 1

Expand Down
37 changes: 36 additions & 1 deletion tests/vw_utilities_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,18 @@
from unittest import TestCase, mock
from unittest.mock import DEFAULT

from volkswagencarnet.vw_utilities import camel2slug, is_valid_path, obj_parser, json_loads, read_config, make_url
from volkswagencarnet.vw_utilities import (
camel2slug,
is_valid_path,
obj_parser,
json_loads,
read_config,
make_url,
fahrenheit_to_vw,
vw_to_celsius,
vw_to_fahrenheit,
celsius_to_vw,
)


class UtilitiesTest(TestCase):
Expand Down Expand Up @@ -130,3 +141,27 @@ def test_make_url(self):
"""Test placeholder replacements."""
self.assertEqual("foo/2/baz", make_url("foo/{bar}/baz{baz}", bar=2, baz=""))
self.assertEqual("foo/asd/2", make_url("foo/{baz}/$bar", bar=2, baz="asd"))

def test_celcius_to_vw(self):
"""Test Celsius conversion."""
self.assertEqual(2730, celsius_to_vw(0))
self.assertEqual(2955, celsius_to_vw(22.4))
self.assertEqual(2960, celsius_to_vw(22.7))

def test_fahrenheit_to_vw(self):
"""Test Fahrenheit conversion."""
self.assertEqual(2730, fahrenheit_to_vw(32))
self.assertEqual(2955, fahrenheit_to_vw(72.3))
self.assertEqual(2960, fahrenheit_to_vw(72.9))

def test_vw_to_celcius(self):
"""Test Celsius conversion."""
self.assertEqual(0, vw_to_celsius(2730))
self.assertEqual(22.5, vw_to_celsius(2955))
self.assertEqual(23, vw_to_celsius(2960))

def test_vw_to_fahrenheit(self):
"""Test Fahrenheit conversion."""
self.assertEqual(32, vw_to_fahrenheit(2730))
self.assertEqual(72, vw_to_fahrenheit(2955))
self.assertEqual(73, vw_to_fahrenheit(2960))
46 changes: 28 additions & 18 deletions volkswagencarnet/vw_connection.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,27 @@
#!/usr/bin/env python3
"""Communicate with We Connect services."""
import asyncio
import hashlib
import logging
import re
import secrets
import sys
import time
import logging
import asyncio
import hashlib
from base64 import b64encode, urlsafe_b64encode
from datetime import timedelta, datetime
from json import dumps as to_json
from random import random
from typing import Optional

import jwt

from sys import version_info
from datetime import timedelta, datetime
from typing import Optional
from urllib.parse import urljoin, parse_qs, urlparse
from json import dumps as to_json
from bs4 import BeautifulSoup
from base64 import b64encode, urlsafe_b64encode

import jwt
from aiohttp import ClientSession, ClientTimeout, client_exceptions
from aiohttp.hdrs import METH_GET, METH_POST
from volkswagencarnet.vw_timer import TimerData
from bs4 import BeautifulSoup

from volkswagencarnet.vw_exceptions import AuthenticationException
from .vw_utilities import json_loads, read_config
from .vw_vehicle import Vehicle

from volkswagencarnet.vw_timer import TimerData, TimersAndProfiles
from .vw_const import (
BRAND,
COUNTRY,
Expand All @@ -41,6 +36,8 @@
USER_AGENT,
APP_URI,
)
from .vw_utilities import json_loads, read_config
from .vw_vehicle import Vehicle

version_info >= (3, 0) or exit("Python 3 required")

Expand All @@ -50,6 +47,7 @@
JWT_ALGORITHMS = ["RS256"]


# noinspection PyPep8Naming
class Connection:
"""Connection to VW-Group Connect services."""

Expand Down Expand Up @@ -1068,7 +1066,19 @@ async def setPreHeater(self, vin, data, spin):
self._session_headers["Content-Type"] = content_type
raise

async def setSchedule(self, vin, data: TimerData):
async def setTimersAndProfiles(self, vin, data: TimersAndProfiles):
"""Set schedules."""
return await self._setDepartureTimer(vin, data, "setTimersAndProfiles")

async def setChargeMinLevel(self, vin, limit: int):
"""Set schedules."""
data: Optional[TimerData] = await self.getTimers(vin)
if data is None:
raise Exception("No existing timer data?")
data.timersAndProfiles.timerBasicSetting.set_charge_min_limit(limit)
return await self._setDepartureTimer(vin, data.timersAndProfiles, "setChargeMinLimit")

async def _setDepartureTimer(self, vin, data: TimersAndProfiles, action: str):
"""Set schedules."""
try:
await self.set_token("vwg")
Expand All @@ -1077,8 +1087,8 @@ async def setSchedule(self, vin, data: TimerData):
vin=vin,
json={
"action": {
"timersAndProfiles": data.timersAndProfiles.json_updated["timer"],
"type": "setTimersAndProfiles",
"timersAndProfiles": data.json_updated["timer"],
"type": action,
}
},
)
Expand Down
38 changes: 36 additions & 2 deletions volkswagencarnet/vw_dashboard.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
# Utilities for integration with Home Assistant
"""Utilities for integration with Home Assistant."""
# Thanks to molobrakos

import logging
from typing import Union

from volkswagencarnet.vw_timer import Timer, TimerData

from .vw_utilities import camel2slug

CLIMA_DEFAULT_DURATION = 30
Expand All @@ -14,6 +13,8 @@


class Instrument:
"""Base class for all components."""

def __init__(self, component, attr, name, icon=None):
self.attr = attr
self.component = component
Expand Down Expand Up @@ -510,33 +511,40 @@ def attributes(self):


class DepartureTimer(Switch):
"""Departure timers."""

def __init__(self, id: Union[str, int]):
self._id = id
super().__init__(attr=f"departure_timer{id}", name=f"Departure Schedule {id}", icon="mdi:car-clock")

@property
def state(self):
"""Return switch state."""
s: Timer = self.vehicle.schedule(self._id)
return 1 if s.enabled else 0

async def turn_on(self):
"""Enable schedule."""
schedule: TimerData = self.vehicle.attrs["timer"]
schedule.get_schedule(self._id).enable()
await self.vehicle.set_schedule(schedule)
await self.vehicle.update()

async def turn_off(self):
"""Disable schedule."""
schedule: TimerData = self.vehicle.attrs["timer"]
schedule.get_schedule(self._id).disable()
await self.vehicle.set_schedule(schedule)
await self.vehicle.update()

@property
def assumed_state(self):
"""Don't assume state info."""
return False

@property
def attributes(self):
"""Schedule attributes."""
s: Timer = self.vehicle.schedule(self._id)
return dict(
# last_result="FIXME",
Expand Down Expand Up @@ -680,6 +688,31 @@ def attributes(self):
return dict(self.vehicle.request_results)


class ChargeMinLevel(Sensor):
"""Get minimum charge level."""

def __init__(self):
"""Init."""
super().__init__(
attr="schedule_min_charge_level",
name="Minimum charge level for departure timers",
icon="mdi:battery-arrow-up",
unit="%",
)

@property
def state(self) -> Union[int, str]:
"""Return the desired minimum charge level."""
if self.vehicle.is_timer_basic_settings_supported:
return self.vehicle.timer_basic_settings.chargeMinLimit
return "Unknown"

@property
def assumed_state(self):
"""Don't assume anything about state."""
return False


def create_instruments():
return [
Position(),
Expand All @@ -695,6 +728,7 @@ def create_instruments():
# ElectricClimatisationClimate(),
# CombustionClimatisationClimate(),
Charging(),
ChargeMinLevel(),
DepartureTimer(1),
DepartureTimer(2),
DepartureTimer(3),
Expand Down
34 changes: 25 additions & 9 deletions volkswagencarnet/vw_timer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from datetime import datetime
from typing import Union, List, Optional, Dict

from volkswagencarnet.vw_utilities import celsius_to_vw, fahrenheit_to_vw, vw_to_celsius

_LOGGER = logging.getLogger(__name__)


Expand Down Expand Up @@ -68,13 +70,27 @@ class BasicSettings(DepartureTimerClass):
def __init__(self, timestamp: str, chargeMinLimit: Union[str, int], targetTemperature: Union[str, int]):
"""Init."""
self.timestamp = timestamp
self.chargeMinLimit = int(chargeMinLimit)
self.targetTemperature = int(targetTemperature)
self.chargeMinLimit: int = int(chargeMinLimit)
self.targetTemperature: int = int(targetTemperature)

@property
def target_temperature_celsius(self):
"""Get target temperature in Celsius."""
return vw_to_celsius(self.targetTemperature)

def set_target_temperature(self, temp: int):
def set_target_temperature_celsius(self, temp: float):
"""Set target temperature for departure timers with climatisation enabled."""
self.targetTemperature = temp
self._changed = True
new_temp = celsius_to_vw(temp)
if new_temp != self.targetTemperature:
self.targetTemperature = new_temp
self._changed = True

def set_target_temperature_fahrenheit(self, temp: float):
"""Set target temperature for departure timers with climatisation enabled."""
new_temp = fahrenheit_to_vw(temp)
if new_temp != self.targetTemperature:
self.targetTemperature = new_temp
self._changed = True

def set_charge_min_limit(self, limit: int):
"""Set the global minimum charge limit."""
Expand Down Expand Up @@ -183,7 +199,7 @@ def __init__(self, timerProfile: List[Union[dict, TimerProfile]]):


# noinspection PyPep8Naming
class TimerAndProfiles(DepartureTimerClass):
class TimersAndProfiles(DepartureTimerClass):
"""Timer and profile object."""

def __init__(
Expand All @@ -206,13 +222,13 @@ def __init__(
class TimerData(DepartureTimerClass):
"""Top level timer object."""

def __init__(self, timersAndProfiles: Union[Dict, TimerAndProfiles], status: Optional[dict]):
def __init__(self, timersAndProfiles: Union[Dict, TimersAndProfiles], status: Optional[dict]):
"""Init."""
try:
self.timersAndProfiles = (
timersAndProfiles
if isinstance(timersAndProfiles, TimerAndProfiles)
else TimerAndProfiles(**timersAndProfiles)
if isinstance(timersAndProfiles, TimersAndProfiles)
else TimersAndProfiles(**timersAndProfiles)
)
self.status = status
self._valid = True
Expand Down
22 changes: 22 additions & 0 deletions volkswagencarnet/vw_utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,25 @@ def make_url(url: str, **kwargs):
if "{" in url or "}" in url:
raise ValueError("Not all values were substituted")
return url


# TODO: is VW using 273.15 or 273? :)
def celsius_to_vw(val: float) -> int:
"""Convert Celsius to VW format."""
return int(5 * round(2 * (273.15 + val)))


def fahrenheit_to_vw(val: float) -> int:
"""Convert Fahrenheit to VW format."""
return int(5 * round(2 * (273.15 + (val - 32) * 5 / 9)))


def vw_to_celsius(val: int) -> float:
"""Convert Celsius to VW format."""
return round(2 * ((val / 10) - 273.15)) / 2


# TODO: are F ints of floats?
def vw_to_fahrenheit(val: int) -> int:
"""Convert Fahrenheit to VW format."""
return int(round((val / 10 - 273.15) * 9 / 5 + 32))
Loading

0 comments on commit 1a8885a

Please sign in to comment.