diff --git a/scenes/entities/sensor.tscn b/scenes/entities/sensor.tscn new file mode 100644 index 0000000..1efb530 --- /dev/null +++ b/scenes/entities/sensor.tscn @@ -0,0 +1,15 @@ +[gd_scene load_steps=3 format=3 uid="uid://xsiy71rsqulj"] + +[ext_resource type="Script" path="res://src/entities/sensor.gd" id="1_57ac8"] + +[sub_resource type="SphereShape3D" id="SphereShape3D_r20gc"] +radius = 0.1 + +[node name="Sensor" type="StaticBody3D"] +script = ExtResource("1_57ac8") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="."] +shape = SubResource("SphereShape3D_r20gc") + +[node name="Label" type="Label3D" parent="."] +text = "some text" diff --git a/src/entities/sensor.gd b/src/entities/sensor.gd new file mode 100644 index 0000000..5aa4d22 --- /dev/null +++ b/src/entities/sensor.gd @@ -0,0 +1,16 @@ +extends StaticBody3D + +@export var entity_id = "sensor.sun_next_dawn" +@onready var label: Label3D = $Label + +# Called when the node enters the scene tree for the first time. +func _ready(): + var stateInfo = await HomeAdapters.adapter_ws.get_state(entity_id) + label.text = stateInfo["state"] + + await HomeAdapters.adapter_ws.watch_state(entity_id, func(new_state): + label.text = new_state["state"] + ) + +func _on_toggle(): + pass \ No newline at end of file diff --git a/src/entities/switch.gd b/src/entities/switch.gd index aced8f0..b9367f7 100644 --- a/src/entities/switch.gd +++ b/src/entities/switch.gd @@ -11,6 +11,13 @@ func _ready(): else: sprite.set_frame(1) + await HomeAdapters.adapter_ws.watch_state(entity_id, func(new_state): + if new_state["state"] == "on": + sprite.set_frame(0) + else: + sprite.set_frame(1) + ) + func _on_toggle(): HomeAdapters.adapter_ws.set_state(entity_id, "off" if sprite.get_frame() == 0 else "on") diff --git a/src/home_adapters/hass_ws/hass.gd b/src/home_adapters/hass_ws/hass.gd index 7f25671..d7323ad 100644 --- a/src/home_adapters/hass_ws/hass.gd +++ b/src/home_adapters/hass_ws/hass.gd @@ -7,7 +7,7 @@ var request_timeout := 10.0 var url := "ws://192.168.33.33:8123/api/websocket" var token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIzZjQ0ZGM2N2Y3YzY0MDc1OGZlMWI2ZjJlNmIxZjRkNSIsImlhdCI6MTY5ODAxMDcyOCwiZXhwIjoyMDEzMzcwNzI4fQ.K6ydLUC-4Q7BNIRCU1nWlI2s6sg9UCiOu-Lpedw2zJc" - +var LOG_MESSAGES := false var authenticated := false var id := 1 @@ -30,14 +30,22 @@ func connect_ws(): print("Connecting to %s" % self.url) socket.connect_to_url(self.url) + # https://github.com/godotengine/godot/issues/84423 + # Otherwise the WebSocketPeer will crash when receiving large packets + socket.set_inbound_buffer_size(65535 * 2) + 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()) + var packet = decode_packet(socket.get_packet()) + if typeof(packet) == TYPE_DICTIONARY: + handle_packet(packet) + elif typeof(packet) == TYPE_ARRAY: + for p in packet: + handle_packet(p) elif state == WebSocketPeer.STATE_CLOSING: pass elif state == WebSocketPeer.STATE_CLOSED: @@ -46,10 +54,8 @@ func _process(delta): print("WS connection closed with code: %s, reason: %s" % [code, reason]) handle_disconnect() -func handle_packet(raw_packet: PackedByteArray): - var packet = decode_packet(raw_packet) - - print("Received packet: %s" % packet) +func handle_packet(packet: Dictionary): + if LOG_MESSAGES: print("Received packet: %s" % packet) if packet.type == "auth_required": send_packet({ @@ -64,17 +70,22 @@ func handle_packet(raw_packet: PackedByteArray): elif packet.type == "auth_invalid": handle_disconnect() else: - packet_callbacks.call_key(packet.id, [packet]) + 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": "supported_features", + # "features": { + # "coalesce_messages": 1 + # } + # }) + + # await send_request_packet({ + # "type": "subscribe_events", + # "event_type": "state_changed" + # }) send_subscribe_packet({ "type": "subscribe_entities" @@ -84,14 +95,23 @@ func start_subscriptions(): if packet.event.has("a"): for entity in packet.event.a.keys(): - entities[entity] = packet.event.a[entity] + entities[entity] = { + "state": packet.event.a[entity]["s"], + "attributes": packet.event.a[entity]["a"] + } entitiy_callbacks.call_key(entity, [entities[entity]]) on_connect.emit() if packet.event.has("c"): for entity in packet.event.c.keys(): + if !entities.has(entity): + continue + if packet.event.c[entity].has("+"): - entities[entity].merge(packet.event.c[entity]["+"]) + if packet.event.c[entity]["+"].has("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"]) entitiy_callbacks.call_key(entity, [entities[entity]]) ) @@ -104,8 +124,8 @@ func send_subscribe_packet(packet: Dictionary, callback: Callable): packet.id = id id += 1 - send_packet(packet) packet_callbacks.add(packet.id, callback) + send_packet(packet) return func(): packet_callbacks.remove(packet.id, callback) @@ -141,7 +161,7 @@ func send_request_packet(packet: Dictionary): func send_packet(packet: Dictionary): - print("Sending packet: %s" % encode_packet(packet)) + if LOG_MESSAGES || true: print("Sending packet: %s" % encode_packet(packet)) socket.send_text(encode_packet(packet)) func decode_packet(packet: PackedByteArray): @@ -151,7 +171,10 @@ func encode_packet(packet: Dictionary): return JSON.stringify(packet) func load_devices(): - pass + if !authenticated: + await on_connect + + return entities func get_state(entity: String): if !authenticated: @@ -159,8 +182,7 @@ func get_state(entity: String): if entities.has(entity): return entities[entity] - else: - print(entities, entity) + return null func watch_state(entity: String, callback: Callable): @@ -174,7 +196,18 @@ func set_state(entity: String, state: String, attributes: Dictionary = {}): assert(authenticated, "Not authenticated") var domain = entity.split(".")[0] - var service = entity.split(".")[1] + var service: String + + if domain == 'switch': + if state == 'on': + service = 'turn_on' + elif state == 'off': + service = 'turn_off' + elif domain == 'light': + if state == 'on': + service = 'turn_on' + elif state == 'off': + service = 'turn_off' return await send_request_packet({ "type": "call_service", diff --git a/src/menu.gd b/src/menu.gd index b50aff6..5944239 100644 --- a/src/menu.gd +++ b/src/menu.gd @@ -4,6 +4,7 @@ const Device = preload("res://scenes/device.tscn") const Entity = preload("res://scenes/entity.tscn") const Switch = preload("res://scenes/entities/switch.tscn") const Light = preload("res://scenes/entities/light.tscn") +const Sensor = preload("res://scenes/entities/sensor.tscn") @onready var devices_node = $Devices var devices @@ -93,6 +94,13 @@ func _on_entity_click(entity_name): light.set_position(global_position) get_node("/root").add_child(light) + + if type == "sensor": + var sensor = Sensor.instantiate() + sensor.entity_id = entity_name + + sensor.set_position(global_position) + get_node("/root").add_child(sensor) func clear_menu(): for child in devices_node.get_children():