From 93604be82dc3c47190f5edd02c94092870d6a423 Mon Sep 17 00:00:00 2001 From: Nitwel Date: Mon, 11 Mar 2024 18:09:50 +0100 Subject: [PATCH] add hass integration support --- content/main.gd | 27 ++++-- content/main.tscn | 2 +- content/ui/components/input/text_handler.gd | 2 - content/ui/menu/edit/edit_menu.gd | 2 +- lib/home_apis/hass_ws/callback_map.gd | 17 ++-- lib/home_apis/hass_ws/handlers/auth.gd | 28 ++++++ lib/home_apis/hass_ws/handlers/integration.gd | 19 ++++ lib/home_apis/hass_ws/hass.gd | 90 +++++++++---------- 8 files changed, 121 insertions(+), 66 deletions(-) create mode 100644 lib/home_apis/hass_ws/handlers/auth.gd create mode 100644 lib/home_apis/hass_ws/handlers/integration.gd diff --git a/content/main.gd b/content/main.gd index f46b74a..6cc7fcf 100644 --- a/content/main.gd +++ b/content/main.gd @@ -1,7 +1,7 @@ extends Node3D -var sky = preload("res://assets/materials/sky.material") -var sky_passthrough = preload("res://assets/materials/sky_passthrough.material") +var sky = preload ("res://assets/materials/sky.material") +var sky_passthrough = preload ("res://assets/materials/sky_passthrough.material") @onready var environment: WorldEnvironment = $WorldEnvironment @onready var camera: XRCamera3D = $XROrigin3D/XRCamera3D @@ -11,6 +11,8 @@ var sky_passthrough = preload("res://assets/materials/sky_passthrough.material") @onready var menu = $Menu @onready var keyboard = $Keyboard +var last_room = null + func _ready(): # In case we're running on the headset, use the passthrough sky if OS.get_name() == "Android": @@ -43,7 +45,7 @@ func _ready(): if action.name == "menu_button": toggle_menu() elif action.name == "by_button": - House.body.mini_view = !House.body.mini_view + House.body.mini_view=!House.body.mini_view ) EventSystem.on_focus_in.connect(func(event): @@ -51,7 +53,7 @@ func _ready(): return add_child(keyboard) - keyboard.global_transform = menu.get_node("AnimationContainer/KeyboardPlace").global_transform + keyboard.global_transform=menu.get_node("AnimationContainer/KeyboardPlace").global_transform ) EventSystem.on_focus_out.connect(func(event): @@ -70,7 +72,7 @@ func toggle_menu(): if menu.show_menu == false: remove_child(menu) -func _emit_action(name: String, value, right_controller: bool = true): +func _emit_action(name: String, value, right_controller: bool=true): var event = EventAction.new() event.name = name event.value = value @@ -82,6 +84,19 @@ func _emit_action(name: String, value, right_controller: bool = true): TYPE_FLOAT, TYPE_VECTOR2: EventSystem.emit("action_value", event) +func _physics_process(delta): + var room = House.body.find_room_at(camera.global_position) + + if room != last_room: + if room: + print("Room changed to: ", room.name) + HomeApi.api.update_room(room.name) + last_room = room + else: + print("Room changed to: ", "outside") + HomeApi.api.update_room("outside") + last_room = null + func _process(delta): if OS.get_name() != "Android": @@ -128,7 +143,7 @@ func vector_key_mapping(key_positive_x: int, key_negative_x: int, key_positive_y elif Input.is_physical_key_pressed(key_negative_x): x = -1 - var vec = Vector3(x, 0 , y) + var vec = Vector3(x, 0, y) if vec: vec = vec.normalized() diff --git a/content/main.tscn b/content/main.tscn index 9ac6eb6..1fe7a43 100644 --- a/content/main.tscn +++ b/content/main.tscn @@ -47,7 +47,7 @@ shadow_enabled = true [node name="XROrigin3D" type="XROrigin3D" parent="."] [node name="XRCamera3D" type="XRCamera3D" parent="XROrigin3D"] -transform = Transform3D(1, 1.8976e-10, 4.07454e-10, 6.76872e-11, 1, 2.08734e-08, -5.82077e-11, 1.09139e-11, 1, 0.0356618, 0.71033, 0.00564247) +transform = Transform3D(0.999992, 0.00357422, -0.00141238, -0.00357241, 0.999993, 0.00127761, 0.00141693, -0.00127253, 0.999998, 0.0356617, 0.71033, 0.00564247) cull_mask = 524287 current = true diff --git a/content/ui/components/input/text_handler.gd b/content/ui/components/input/text_handler.gd index 18ac058..51bef8b 100644 --- a/content/ui/components/input/text_handler.gd +++ b/content/ui/components/input/text_handler.gd @@ -33,8 +33,6 @@ func set_text(value: String, insert: bool=false): overflow_index = _calculate_overflow_index() focus_caret() - print(overflow_index, " ", char_offset, " ", caret_position) - func get_display_text(): # In case all chars fit, return the whole text. if overflow_index == - 1: diff --git a/content/ui/menu/edit/edit_menu.gd b/content/ui/menu/edit/edit_menu.gd index 6db33e9..0a08a91 100644 --- a/content/ui/menu/edit/edit_menu.gd +++ b/content/ui/menu/edit/edit_menu.gd @@ -154,4 +154,4 @@ func _on_entity_click(entity_name): func clear_menu(): for child in devices_node.get_children(): devices_node.remove_child(child) - child.queue_free() \ No newline at end of file + child.queue_free() diff --git a/lib/home_apis/hass_ws/callback_map.gd b/lib/home_apis/hass_ws/callback_map.gd index ee85c12..6979927 100644 --- a/lib/home_apis/hass_ws/callback_map.gd +++ b/lib/home_apis/hass_ws/callback_map.gd @@ -3,6 +3,7 @@ extends Node class_name CallbackMap var callbacks := {} +var single_callbacks: Array = [] func add(key: Variant, callback: Callable) -> void: _validate_key(key) @@ -15,13 +16,9 @@ func add(key: Variant, callback: Callable) -> void: func add_once(key: Variant, callback: Callable) -> void: _validate_key(key) - var fn: Callable - - fn = func(args: Array): - remove(key, fn) - callback.callv(args) + single_callbacks.append(callback) - add(key, fn) + add(key, callback) func remove(key: Variant, callback: Callable) -> void: _validate_key(key) @@ -29,6 +26,9 @@ func remove(key: Variant, callback: Callable) -> void: if callbacks.has(key): callbacks[key].erase(callback) + if single_callbacks.has(callback): + single_callbacks.erase(callback) + func call_key(key: Variant, args: Array) -> void: _validate_key(key) @@ -36,5 +36,8 @@ func call_key(key: Variant, args: Array) -> void: for callback in callbacks[key]: callback.callv(args) + if single_callbacks.has(callback): + remove(key, callback) + func _validate_key(key: Variant): - assert(typeof(key) == TYPE_STRING || typeof(key) == TYPE_INT || typeof(key) == TYPE_FLOAT, "key must be a string or number") + assert(typeof(key) == TYPE_STRING||typeof(key) == TYPE_INT||typeof(key) == TYPE_FLOAT, "key must be a string or number") diff --git a/lib/home_apis/hass_ws/handlers/auth.gd b/lib/home_apis/hass_ws/handlers/auth.gd new file mode 100644 index 0000000..036ccd2 --- /dev/null +++ b/lib/home_apis/hass_ws/handlers/auth.gd @@ -0,0 +1,28 @@ +const HASS_API = preload ("../hass.gd") + +signal on_authenticated() + +var api: HASS_API +var url: String +var token: String + +var authenticated := false + +func _init(hass: HASS_API, url: String, token: String): + self.api = hass + self.url = url + self.token = token + +func handle_message(message): + match message["type"]: + "auth_required": + api.send_packet({"type": "auth", "access_token": self.token}) + "auth_ok": + authenticated = true + on_authenticated.emit() + "auth_invalid": + EventSystem.notify("Failed to authenticate with Home Assistant. Check your token and try again.", EventNotify.Type.DANGER) + api.handle_disconnect() + +func on_disconnect(): + authenticated = false \ No newline at end of file diff --git a/lib/home_apis/hass_ws/handlers/integration.gd b/lib/home_apis/hass_ws/handlers/integration.gd new file mode 100644 index 0000000..7e55162 --- /dev/null +++ b/lib/home_apis/hass_ws/handlers/integration.gd @@ -0,0 +1,19 @@ +const HASS_API = preload ("../hass.gd") + +var api: HASS_API +var integration_exists: bool = false + +func _init(hass: HASS_API): + self.api = hass + +func on_connect(): + var response = await api.send_request_packet({ + "type": "immersive_home/register", + "device_id": OS.get_unique_id(), + "name": OS.get_model_name(), + "version": OS.get_version(), + "platform": OS.get_name(), + }) + + if response.status == Promise.Status.RESOLVED: + integration_exists = true \ No newline at end of file diff --git a/lib/home_apis/hass_ws/hass.gd b/lib/home_apis/hass_ws/hass.gd index 7c26a2a..ca1402c 100644 --- a/lib/home_apis/hass_ws/hass.gd +++ b/lib/home_apis/hass_ws/hass.gd @@ -1,5 +1,8 @@ extends Node +const AuthHandler = preload ("./handlers/auth.gd") +const IntegrationHandler = preload ("./handlers/integration.gd") + signal on_connect() signal on_disconnect() var connected := false @@ -13,25 +16,32 @@ var request_timeout := 10.0 var url := "" var token := "" - var LOG_MESSAGES := false -var authenticated := false - var id := 1 var entities: Dictionary = {} var entitiy_callbacks := CallbackMap.new() var packet_callbacks := CallbackMap.new() -func _init(url := self.url, token := self.token): +var auth_handler: AuthHandler +var integration_handler: IntegrationHandler + +func _init(url:=self.url, token:=self.token): self.url = url self.token = token + auth_handler = AuthHandler.new(self, url, token) + integration_handler = IntegrationHandler.new(self) + devices_template = devices_template.replace("\n", " ").replace("\t", "").replace("\r", " ") connect_ws() + auth_handler.on_authenticated.connect(func(): + start_subscriptions() + ) + func connect_ws(): - if url == "" || token == "": + if url == ""||token == "": return print("Connecting to %s" % url + "/api/websocket") @@ -71,38 +81,12 @@ func _process(delta): func handle_packet(packet: Dictionary): if LOG_MESSAGES: print("Received packet: %s" % str(packet).substr(0, 1000)) - if packet.type == "auth_required": - send_packet({ - "type": "auth", - "access_token": self.token - }) + auth_handler.handle_message(packet) - elif packet.type == "auth_ok": - authenticated = true - start_subscriptions() - - elif packet.type == "auth_invalid": - EventSystem.notify("Failed to authenticate, invalid auth token", EventNotify.Type.DANGER) - print("Failed to authenticate, invalid auth token") - handle_disconnect() - else: + if packet.has("id"): packet_callbacks.call_key(int(packet.id), [packet]) func start_subscriptions(): - assert(authenticated, "Not authenticated") - - # await send_request_packet({ - # "type": "supported_features", - # "features": { - # "coalesce_messages": 1 - # } - # }) - - # await send_request_packet({ - # "type": "subscribe_events", - # "event_type": "state_changed" - # }) - send_subscribe_packet({ "type": "subscribe_entities" }, func(packet: Dictionary): @@ -111,13 +95,12 @@ func start_subscriptions(): if packet.event.has("a"): for entity in packet.event.a.keys(): - entities[entity] = { + entities[entity]={ "state": packet.event.a[entity]["s"], "attributes": packet.event.a[entity]["a"] } entitiy_callbacks.call_key(entity, [entities[entity]]) - connected = true - on_connect.emit() + handle_connect() if packet.event.has("c"): for entity in packet.event.c.keys(): @@ -126,14 +109,19 @@ func start_subscriptions(): if packet.event.c[entity].has("+"): if packet.event.c[entity]["+"].has("s"): - entities[entity]["state"] = packet.event.c[entity]["+"]["s"] + entities[entity]["state"]=packet.event.c[entity]["+"]["s"] if packet.event.c[entity]["+"].has("a"): entities[entity]["attributes"].merge(packet.event.c[entity]["+"]["a"], true) entitiy_callbacks.call_key(entity, [entities[entity]]) ) +func handle_connect(): + integration_handler.on_connect() + connected = true + on_connect.emit() + func handle_disconnect(): - authenticated = false + auth_handler.on_disconnect() set_process(false) on_disconnect.emit() @@ -153,18 +141,15 @@ func send_subscribe_packet(packet: Dictionary, callback: Callable): }) id += 1 - -func send_request_packet(packet: Dictionary, ignore_initial := false): +func send_request_packet(packet: Dictionary, ignore_initial:=false): packet.id = id id += 1 - send_packet(packet) - var promise = Promise.new(func(resolve: Callable, reject: Callable): var fn: Callable if ignore_initial: - fn = func(packet: Dictionary): + fn=func(packet: Dictionary): if packet.type == "event": resolve.call(packet) packet_callbacks.remove(packet.id, fn) @@ -173,7 +158,7 @@ func send_request_packet(packet: Dictionary, ignore_initial := false): else: packet_callbacks.add_once(packet.id, resolve) - var timeout = Timer.new() + var timeout=Timer.new() timeout.set_wait_time(request_timeout) timeout.set_one_shot(true) timeout.timeout.connect(func(): @@ -187,8 +172,9 @@ func send_request_packet(packet: Dictionary, ignore_initial := false): timeout.start() ) - return await promise.settled + send_packet(packet) + return await promise.settled func send_packet(packet: Dictionary): if LOG_MESSAGES: print("Sending packet: %s" % encode_packet(packet)) @@ -221,15 +207,13 @@ func get_state(entity: String): return entities[entity] return null - func watch_state(entity: String, callback: Callable): entitiy_callbacks.add(entity, callback) return func(): entitiy_callbacks.remove(entity, callback) - -func set_state(entity: String, state: String, attributes: Dictionary = {}): +func set_state(entity: String, state: String, attributes: Dictionary={}): var domain = entity.split(".")[0] var service: String @@ -271,4 +255,12 @@ func set_state(entity: String, state: String, attributes: Dictionary = {}): } }) - +func update_room(room: String): + var response = await send_request_packet({ + "type": "immersive_home/update", + "device_id": OS.get_unique_id(), + "room": room + }) + + if response.status == Promise.Status.RESOLVED: + print("Room updated") \ No newline at end of file