diff --git a/promise/LICENSE b/addons/promise/LICENSE similarity index 100% rename from promise/LICENSE rename to addons/promise/LICENSE diff --git a/promise/promise.gd b/addons/promise/promise.gd similarity index 100% rename from promise/promise.gd rename to addons/promise/promise.gd diff --git a/src/entities/light.gd b/src/entities/light.gd index 5f77c50..aced8f0 100644 --- a/src/entities/light.gd +++ b/src/entities/light.gd @@ -5,7 +5,7 @@ extends StaticBody3D # Called when the node enters the scene tree for the first time. func _ready(): - var stateInfo = await HomeAdapters.adapter.get_state(entity_id) + var stateInfo = await HomeAdapters.adapter_ws.get_state(entity_id) if stateInfo["state"] == "on": sprite.set_frame(0) else: @@ -13,7 +13,7 @@ func _ready(): func _on_toggle(): - HomeAdapters.adapter.set_state(entity_id, "off" if sprite.get_frame() == 0 else "on") + HomeAdapters.adapter_ws.set_state(entity_id, "off" if sprite.get_frame() == 0 else "on") if sprite.get_frame() == 0: sprite.set_frame(1) else: diff --git a/src/entities/switch.gd b/src/entities/switch.gd index 5f77c50..aced8f0 100644 --- a/src/entities/switch.gd +++ b/src/entities/switch.gd @@ -5,7 +5,7 @@ extends StaticBody3D # Called when the node enters the scene tree for the first time. func _ready(): - var stateInfo = await HomeAdapters.adapter.get_state(entity_id) + var stateInfo = await HomeAdapters.adapter_ws.get_state(entity_id) if stateInfo["state"] == "on": sprite.set_frame(0) else: @@ -13,7 +13,7 @@ func _ready(): func _on_toggle(): - HomeAdapters.adapter.set_state(entity_id, "off" if sprite.get_frame() == 0 else "on") + HomeAdapters.adapter_ws.set_state(entity_id, "off" if sprite.get_frame() == 0 else "on") if sprite.get_frame() == 0: sprite.set_frame(1) else: diff --git a/src/globals/home_adapters.gd b/src/globals/home_adapters.gd index 3b54b51..11a4922 100644 --- a/src/globals/home_adapters.gd +++ b/src/globals/home_adapters.gd @@ -6,21 +6,6 @@ var adapter = Adapter.new(Adapter.ADAPTER_TYPES.HASS) var adapter_ws = Adapter.new(Adapter.ADAPTER_TYPES.HASS_WS) func _ready(): + add_child(adapter) add_child(adapter_ws) - - var timer = Timer.new() - timer.set_wait_time(1) - timer.set_one_shot(true) - - print("timer started") - timer.timeout.connect(func(): - print("timer done") - - var result = await adapter_ws.get_state("light.living_room") - print(result) - ) - - add_child(timer) - timer.start() - diff --git a/src/home_adapters/hass_ws/callback_map.gd b/src/home_adapters/hass_ws/callback_map.gd new file mode 100644 index 0000000..ee85c12 --- /dev/null +++ b/src/home_adapters/hass_ws/callback_map.gd @@ -0,0 +1,40 @@ +extends Node + +class_name CallbackMap + +var callbacks := {} + +func add(key: Variant, callback: Callable) -> void: + _validate_key(key) + + if callbacks.has(key): + callbacks[key].append(callback) + else: + callbacks[key] = [callback] + +func add_once(key: Variant, callback: Callable) -> void: + _validate_key(key) + + var fn: Callable + + fn = func(args: Array): + remove(key, fn) + callback.callv(args) + + add(key, fn) + +func remove(key: Variant, callback: Callable) -> void: + _validate_key(key) + + if callbacks.has(key): + callbacks[key].erase(callback) + +func call_key(key: Variant, args: Array) -> void: + _validate_key(key) + + if callbacks.has(key): + for callback in callbacks[key]: + callback.callv(args) + +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") diff --git a/src/home_adapters/hass_ws/hass.gd b/src/home_adapters/hass_ws/hass.gd index b54e920..7f25671 100644 --- a/src/home_adapters/hass_ws/hass.gd +++ b/src/home_adapters/hass_ws/hass.gd @@ -1,18 +1,23 @@ extends Node -var devices_template = FileAccess.get_file_as_string("res://src/home_adapters/hass/templates/devices.j2") -var socket = WebSocketPeer.new() +var devices_template := FileAccess.get_file_as_string("res://src/home_adapters/hass/templates/devices.j2") +var socket := WebSocketPeer.new() # in seconds -var request_timeout: float = 10 +var request_timeout := 10.0 -var url: String = "ws://192.168.33.33:8123/api/websocket" -var token: String = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIzZjQ0ZGM2N2Y3YzY0MDc1OGZlMWI2ZjJlNmIxZjRkNSIsImlhdCI6MTY5ODAxMDcyOCwiZXhwIjoyMDEzMzcwNzI4fQ.K6ydLUC-4Q7BNIRCU1nWlI2s6sg9UCiOu-Lpedw2zJc" +var url := "ws://192.168.33.33:8123/api/websocket" +var token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIzZjQ0ZGM2N2Y3YzY0MDc1OGZlMWI2ZjJlNmIxZjRkNSIsImlhdCI6MTY5ODAxMDcyOCwiZXhwIjoyMDEzMzcwNzI4fQ.K6ydLUC-4Q7BNIRCU1nWlI2s6sg9UCiOu-Lpedw2zJc" -var authenticated: bool = false -var id = 1 +var authenticated := false +var id := 1 +var entities: Dictionary = {} -signal packet_received(packet: Dictionary) +var entitiy_callbacks := CallbackMap.new() +var packet_callbacks := CallbackMap.new() + +signal on_connect() +signal on_disconnect() func _init(url := self.url, token := self.token): self.url = url @@ -27,8 +32,9 @@ func connect_ws(): func _process(delta): socket.poll() - + var state = socket.get_ready_state() + print(state, "POLLING") if state == WebSocketPeer.STATE_OPEN: while socket.get_available_packet_count(): handle_packet(socket.get_packet()) @@ -38,8 +44,7 @@ func _process(delta): var code = socket.get_close_code() var reason = socket.get_close_reason() print("WS connection closed with code: %s, reason: %s" % [code, reason]) - authenticated = false - set_process(false) + handle_disconnect() func handle_packet(raw_packet: PackedByteArray): var packet = decode_packet(raw_packet) @@ -54,36 +59,79 @@ func handle_packet(raw_packet: PackedByteArray): elif packet.type == "auth_ok": authenticated = true + start_subscriptions() + elif packet.type == "auth_invalid": - authenticated = false - print("Authentication failed") - set_process(false) + handle_disconnect() else: - packet_received.emit(packet) + packet_callbacks.call_key(packet.id, [packet]) + +func start_subscriptions(): + assert(authenticated, "Not authenticated") + + await send_request_packet({ + "type": "supported_features", + "features": { + "coalesce_messages": 1 + } + }) + + send_subscribe_packet({ + "type": "subscribe_entities" + }, func(packet: Dictionary): + if packet.type != "event": + return + + if packet.event.has("a"): + for entity in packet.event.a.keys(): + entities[entity] = packet.event.a[entity] + entitiy_callbacks.call_key(entity, [entities[entity]]) + on_connect.emit() + + if packet.event.has("c"): + for entity in packet.event.c.keys(): + if packet.event.c[entity].has("+"): + entities[entity].merge(packet.event.c[entity]["+"]) + entitiy_callbacks.call_key(entity, [entities[entity]]) + ) + +func handle_disconnect(): + authenticated = false + set_process(false) + on_disconnect.emit() + +func send_subscribe_packet(packet: Dictionary, callback: Callable): + packet.id = id + id += 1 + + send_packet(packet) + packet_callbacks.add(packet.id, callback) + + return func(): + packet_callbacks.remove(packet.id, callback) + send_packet({ + id: id, + "type": packet.type.replace("subscribe", "unsubscribe"), + "subscription": packet.id + }) + id += 1 + - func send_request_packet(packet: Dictionary): packet.id = id id += 1 send_packet(packet) - var promise = Promise.new(func(resolve: Callable, reject: Callable): - var handle_packet = func(recieved_packet: Dictionary): - print("Received packet in handler: %s" % recieved_packet) - if packet.id == recieved_packet.id: - print("same id") - resolve.call(recieved_packet) - packet_received.disconnect(handle_packet) - - packet_received.connect(handle_packet) + var promise = Promise.new(func(resolve: Callable, reject: Callable): + packet_callbacks.add_once(packet.id, resolve) var timeout = Timer.new() timeout.set_wait_time(request_timeout) timeout.set_one_shot(true) timeout.timeout.connect(func(): reject.call(Promise.Rejection.new("Request timed out")) - packet_received.disconnect(handle_packet) + packet_callbacks.remove(packet.id, resolve) ) add_child(timeout) timeout.start() @@ -106,22 +154,36 @@ func load_devices(): pass func get_state(entity: String): - assert(authenticated, "Not authenticated") + if !authenticated: + await on_connect - var result = await send_request_packet({ - "type": "get_states" - }) - - if result.status == Promise.Status.RESOLVED: - return result.payload - return null + if entities.has(entity): + return entities[entity] + else: + print(entities, entity) func watch_state(entity: String, callback: Callable): - assert(authenticated, "Not authenticated") + if !authenticated: + await on_connect + + entitiy_callbacks.add(entity, callback) func set_state(entity: String, state: String, attributes: Dictionary = {}): assert(authenticated, "Not authenticated") + var domain = entity.split(".")[0] + var service = entity.split(".")[1] + + return await send_request_packet({ + "type": "call_service", + "domain": domain, + "service": service, + "service_data": attributes, + "target": { + "entity_id": entity + } + }) +