diff --git a/src/const.py b/src/const.py index 0f7a233..5a18f45 100644 --- a/src/const.py +++ b/src/const.py @@ -83,6 +83,7 @@ supported_entities = [ {"name": "Estimated Charging Finish Time", "domain": "sensor", "id": "estimated_charging_finish_time", "icon": "timer-sync-outline", "url": RECHARGE_STATE_URL}, {"name": "Odometer", "domain": "sensor", "id": "odometer", "unit": "km" if not units.get(settings["babelLocale"]) else units[settings["babelLocale"]]["odometer"]["unit"], "icon": "counter", "url": ODOMETER_STATE_URL}, {"name": "Last Data Update", "domain": "sensor", "id": "last_data_update", "icon": "timer", "url": ""}, + {"name": "Active schedules", "domain": "sensor", "id": "active_schedules", "icon": "timer", "url": ""}, {"name": "Window Front Left", "domain": "binary_sensor", "device_class": "window", "id": "window_front_left", "icon": "car-door-lock", "url": WINDOWS_STATE_URL}, {"name": "Window Front Right", "domain": "binary_sensor", "device_class": "window", "id": "window_front_right", "icon": "car-door-lock", "url": WINDOWS_STATE_URL}, {"name": "Window Rear Left", "domain": "binary_sensor", "device_class": "window", "id": "window_rear_left", "icon": "car-door-lock", "url": WINDOWS_STATE_URL}, diff --git a/src/mqtt.py b/src/mqtt.py index bef609c..571010a 100644 --- a/src/mqtt.py +++ b/src/mqtt.py @@ -19,6 +19,7 @@ last_data_update = None climate_timer = {} engine_status = {} devices = {} +active_schedules = {} def connect(): @@ -99,58 +100,107 @@ def on_message(client, userdata, msg): payload = msg.payload.decode("UTF-8") if "climate_status" in msg.topic: - global assumed_climate_state, climate_timer, engine_status if payload == "ON": - # Start the api call in another thread for HA performance - Thread(target=volvo.api_call, args=(CLIMATE_START_URL, "POST", vin)).start() - - # Start door check thread to turn off climate if driver door is opened - check_engine_thread = Thread(target=volvo.check_engine_status, args=(vin, )) - check_engine_thread.start() - engine_status[vin] = check_engine_thread - - # Starting timer to disable climate after 30 mins - climate_timer[vin] = Timer(30 * 60, volvo.disable_climate, (vin, )) - climate_timer[vin].start() - - # Set and update switch status - assumed_climate_state[vin] = "ON" - update_car_data() + start_climate(vin) elif payload == "OFF": - # Start the api call in another thread for HA performance - Thread(target=volvo.api_call, args=(CLIMATE_STOP_URL, "POST", vin)).start() - - # Stop door check thread if running - if engine_status[vin].is_alive(): - engine_status[vin].do_run = False - - # Stop climate timer if active - if climate_timer[vin].is_alive(): - climate_timer[vin].cancel() - - # Set and update switch status - assumed_climate_state[vin] = "OFF" - update_car_data() + stop_climate(vin) elif "lock_status" in msg.topic: if payload == "LOCK": - # Start the api call in another thread for HA performance - Thread(target=volvo.api_call, args=(CAR_LOCK_URL, "POST", vin)).start() - - # Force set locking state - update_car_data(False, {"entity_id": "lock_status", "vin": vin, "state": "LOCKING"}) - # Fetch API lock state until locking finished - Thread(target=volvo.check_lock_status, args=(vin, "UNLOCKED")).start() + lock_car(vin) elif payload == "UNLOCK": - # Start the api call in another thread for HA performance - Thread(target=volvo.api_call, args=(CAR_UNLOCK_URL, "POST", vin)).start() - - # Force set unlocking state - update_car_data(False, {"entity_id": "lock_status", "vin": vin, "state": "UNLOCKING"}) - # Fetch API lock state until unlocking finished - Thread(target=volvo.check_lock_status, args=(vin, "LOCKED")).start() + unlock_car(vin) elif "update_data" in msg.topic: if payload == "PRESS": update_car_data(True) + elif "schedule" in msg.topic: + try: + d = json.loads(payload) + except ValueError as e: + logging.error("Can't set timer. Error: " + str(e)) + return None + + if d["mode"] == "timer": + start_climate_timer(d, vin) + else: + logging.warning("No schedule mode found, doing nothing") + + +def start_climate_timer(d, vin): + global active_schedules + try: + minute = int(d["start_time"].split(":")[1]) + hour = int(d["start_time"].split(":")[0]) + local_datetime = datetime.now(util.TZ) + start_datetime = local_datetime.replace(hour=hour, minute=minute, second=0) + timer_seconds = (start_datetime - local_datetime).total_seconds() + except Exception as e: + logging.error("Error creating climate timer: " + str(e)) + return None + + if timer_seconds > 0: + Timer(timer_seconds, start_climate, (vin, )).start() + active_schedules[vin]["timers"].append(start_datetime.isoformat()) + logging.debug("Climate timer set to " + str(start_datetime)) + update_car_data() + else: + logging.warning("Timer can not be set. Unusable start time entered") + + +def unlock_car(vin): + # Start the api call in another thread for HA performance + Thread(target=volvo.api_call, args=(CAR_UNLOCK_URL, "POST", vin)).start() + + # Force set unlocking state + update_car_data(False, {"entity_id": "lock_status", "vin": vin, "state": "UNLOCKING"}) + # Fetch API lock state until unlocking finished + Thread(target=volvo.check_lock_status, args=(vin, "LOCKED")).start() + + +def lock_car(vin): + # Start the api call in another thread for HA performance + Thread(target=volvo.api_call, args=(CAR_LOCK_URL, "POST", vin)).start() + + # Force set locking state + update_car_data(False, {"entity_id": "lock_status", "vin": vin, "state": "LOCKING"}) + # Fetch API lock state until locking finished + Thread(target=volvo.check_lock_status, args=(vin, "UNLOCKED")).start() + + +def stop_climate(vin): + global assumed_climate_state, climate_timer, engine_status + # Start the api call in another thread for HA performance + Thread(target=volvo.api_call, args=(CLIMATE_STOP_URL, "POST", vin)).start() + + # Stop door check thread if running + if engine_status[vin].is_alive(): + engine_status[vin].do_run = False + + # Stop climate timer if active + if climate_timer[vin].is_alive(): + climate_timer[vin].cancel() + + # Set and update switch status + assumed_climate_state[vin] = "OFF" + update_car_data() + + +def start_climate(vin): + global assumed_climate_state, climate_timer, engine_status + # Start the api call in another thread for HA performance + Thread(target=volvo.api_call, args=(CLIMATE_START_URL, "POST", vin)).start() + + # Start door check thread to turn off climate if driver door is opened + check_engine_thread = Thread(target=volvo.check_engine_status, args=(vin,)) + check_engine_thread.start() + engine_status[vin] = check_engine_thread + + # Starting timer to disable climate after 30 mins + climate_timer[vin] = Timer(30 * 60, volvo.disable_climate, (vin,)) + climate_timer[vin].start() + + # Set and update switch status + assumed_climate_state[vin] = "ON" + update_car_data() def update_loop(): @@ -183,20 +233,22 @@ def update_car_data(force_update=False, overwrite={}): state = assumed_climate_state[vin] elif entity["id"] == "last_data_update": state = last_data_update + elif entity["id"] == "active_schedules": + state = active_schedules[vin] else: if entity["id"] == ov_entity_id and vin == ov_vin: state = ov_state else: state = volvo.api_call(entity["url"], "GET", vin, entity["id"], force_update) - if entity["domain"] == "device_tracker": + if entity["domain"] == "device_tracker" or entity["id"] == "active_schedules": topic = f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/attributes" else: topic = f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/state" mqtt_client.publish( topic, - json.dumps(state) if isinstance(state, dict) else state + json.dumps(state) if isinstance(state, dict) or isinstance(state, list) else state ) update_ha_device(entity, vin, state) @@ -267,7 +319,7 @@ def create_ha_devices(): if entity.get("unit"): config["unit_of_measurement"] = entity["unit"] - if entity.get("domain") == "device_tracker": + if entity.get("domain") == "device_tracker" or entity.get("id") == "active_schedules": config["json_attributes_topic"] = f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/attributes" elif entity.get("domain") in ["switch", "lock", "button"]: command_topic = f"homeassistant/{entity['domain']}/{vin}_{entity['id']}/command" diff --git a/src/volvo.py b/src/volvo.py index 0207b91..2bb50f8 100644 --- a/src/volvo.py +++ b/src/volvo.py @@ -111,6 +111,7 @@ def get_vehicles(): raise Exception("No vehicle found, exiting application!") else: initialize_climate(vins) + initialize_scheduler(vins) logging.info("Vin: " + str(vins) + " found!") @@ -168,6 +169,12 @@ def check_supported_endpoints(): logging.info("Failed, " + entity["name"] + " is unfortunately not supported by your vehicle.") +def initialize_scheduler(vins): + for vin in vins: + mqtt.active_schedules[vin] = {"timers": []} + mqtt.subscribed_topics = [f"homeassistant/schedule/{vin}/command"] + + def initialize_climate(vins): for vin in vins: mqtt.assumed_climate_state[vin] = "OFF"