Merge pull request #14 from Nitwel/testing

Finish websocket adapter
This commit is contained in:
Nitwel 2023-11-05 16:37:18 +01:00 committed by GitHub
commit fb378954d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 88 additions and 69 deletions

View File

@ -31,6 +31,20 @@ Communication with the Smart Home Environment is done using the `HomeAdapters` g
A device is a collection of different entities and entities can represent many different things in a smart home. A device is a collection of different entities and entities can represent many different things in a smart home.
For example, the entity of name `lights.smart_lamp_1` would control the kitchen lamps while `state.smart_lamp_1_temp` would show the current temperature of the lamp. For example, the entity of name `lights.smart_lamp_1` would control the kitchen lamps while `state.smart_lamp_1_temp` would show the current temperature of the lamp.
### File Structure
```
.
├── addons (All installed Godot Addons are saved here)
├── assets (Files like logos or assets that are shared across scenes)
├── content/ (Main files of the project)
│ ├── entities (Entities that can be placed into the room)
│ └── ui (User Interface Scenes and related files)
└── lib/ (Code that is global or shared across scenes)
├── globals (Globally running scripts)
└── home_adapters (Code allowing control smart home entities)
```
### Home Adapters ### Home Adapters
The `HomeAdapters` global allows to communicate with different backends and offers a set of fundamental functions allowing communication with the Smart Home. The `HomeAdapters` global allows to communicate with different backends and offers a set of fundamental functions allowing communication with the Smart Home.

View File

@ -5,7 +5,7 @@ extends StaticBody3D
# Called when the node enters the scene tree for the first time. # Called when the node enters the scene tree for the first time.
func _ready(): func _ready():
var stateInfo = await HomeAdapters.adapter_ws.get_state(entity_id) var stateInfo = await HomeAdapters.adapter.get_state(entity_id)
if stateInfo["state"] == "on": if stateInfo["state"] == "on":
sprite.set_frame(0) sprite.set_frame(0)
else: else:
@ -13,7 +13,7 @@ func _ready():
func _on_toggle(): func _on_toggle():
HomeAdapters.adapter_ws.set_state(entity_id, "off" if sprite.get_frame() == 0 else "on") HomeAdapters.adapter.set_state(entity_id, "off" if sprite.get_frame() == 0 else "on")
if sprite.get_frame() == 0: if sprite.get_frame() == 0:
sprite.set_frame(1) sprite.set_frame(1)
else: else:

View File

@ -5,10 +5,10 @@ extends StaticBody3D
# Called when the node enters the scene tree for the first time. # Called when the node enters the scene tree for the first time.
func _ready(): func _ready():
var stateInfo = await HomeAdapters.adapter_ws.get_state(entity_id) var stateInfo = await HomeAdapters.adapter.get_state(entity_id)
label.text = stateInfo["state"] label.text = stateInfo["state"]
await HomeAdapters.adapter_ws.watch_state(entity_id, func(new_state): await HomeAdapters.adapter.watch_state(entity_id, func(new_state):
label.text = new_state["state"] label.text = new_state["state"]
) )

View File

@ -5,7 +5,7 @@ extends StaticBody3D
# Called when the node enters the scene tree for the first time. # Called when the node enters the scene tree for the first time.
func _ready(): func _ready():
var stateInfo = await HomeAdapters.adapter_ws.get_state(entity_id) var stateInfo = await HomeAdapters.adapter.get_state(entity_id)
if stateInfo == null: if stateInfo == null:
return return
@ -14,7 +14,7 @@ func _ready():
else: else:
sprite.set_frame(1) sprite.set_frame(1)
await HomeAdapters.adapter_ws.watch_state(entity_id, func(new_state): await HomeAdapters.adapter.watch_state(entity_id, func(new_state):
if new_state["state"] == "on": if new_state["state"] == "on":
sprite.set_frame(0) sprite.set_frame(0)
else: else:
@ -23,7 +23,7 @@ func _ready():
func _on_toggle(): func _on_toggle():
HomeAdapters.adapter_ws.set_state(entity_id, "off" if sprite.get_frame() == 0 else "on") HomeAdapters.adapter.set_state(entity_id, "off" if sprite.get_frame() == 0 else "on")
if sprite.get_frame() == 0: if sprite.get_frame() == 0:
sprite.set_frame(1) sprite.set_frame(1)
else: else:

View File

@ -1,13 +1,11 @@
[gd_scene load_steps=13 format=3 uid="uid://eecv28y6jxk4"] [gd_scene load_steps=11 format=3 uid="uid://eecv28y6jxk4"]
[ext_resource type="PackedScene" uid="uid://clc5dre31iskm" path="res://addons/godot-xr-tools/xr/start_xr.tscn" id="1_i4c04"] [ext_resource type="PackedScene" uid="uid://clc5dre31iskm" path="res://addons/godot-xr-tools/xr/start_xr.tscn" id="1_i4c04"]
[ext_resource type="Script" path="res://content/raycast.gd" id="1_tsqxc"] [ext_resource type="Script" path="res://content/raycast.gd" id="1_tsqxc"]
[ext_resource type="Script" path="res://content/main.gd" id="1_uvrd4"] [ext_resource type="Script" path="res://content/main.gd" id="1_uvrd4"]
[ext_resource type="Script" path="res://content/model.gd" id="2_7f1x4"]
[ext_resource type="PackedScene" uid="uid://c3kdssrmv84kv" path="res://content/ui/menu/menu.tscn" id="3_1tbp3"] [ext_resource type="PackedScene" uid="uid://c3kdssrmv84kv" path="res://content/ui/menu/menu.tscn" id="3_1tbp3"]
[ext_resource type="PackedScene" uid="uid://ctltchlf2j2r4" path="res://addons/xr-simulator/XRSimulator.tscn" id="5_3qc8g"] [ext_resource type="PackedScene" uid="uid://ctltchlf2j2r4" path="res://addons/xr-simulator/XRSimulator.tscn" id="5_3qc8g"]
[ext_resource type="Material" uid="uid://bf5ina366dwm6" path="res://assets/materials/sky.material" id="5_wgwf8"] [ext_resource type="Material" uid="uid://bf5ina366dwm6" path="res://assets/materials/sky.material" id="5_wgwf8"]
[ext_resource type="PackedScene" uid="uid://cscl5k7lhopj5" path="res://content/entities/switch/switch.tscn" id="8_uxmrb"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_m58yb"] [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_m58yb"]
ao_enabled = true ao_enabled = true
@ -43,9 +41,6 @@ pose = &"aim"
[node name="MeshInstance3D" type="MeshInstance3D" parent="XROrigin3D/XRControllerLeft"] [node name="MeshInstance3D" type="MeshInstance3D" parent="XROrigin3D/XRControllerLeft"]
mesh = SubResource("BoxMesh_ir3co") mesh = SubResource("BoxMesh_ir3co")
[node name="Model" type="Node3D" parent="XROrigin3D/XRControllerLeft"]
script = ExtResource("2_7f1x4")
[node name="Menu" parent="XROrigin3D/XRControllerLeft" instance=ExtResource("3_1tbp3")] [node name="Menu" parent="XROrigin3D/XRControllerLeft" instance=ExtResource("3_1tbp3")]
transform = Transform3D(-4.37114e-08, 0, -1, -0.707107, 0.707107, 3.09086e-08, 0.707107, 0.707107, -3.09086e-08, 0.183517, 0, -0.0534939) transform = Transform3D(-4.37114e-08, 0, -1, -0.707107, 0.707107, 3.09086e-08, 0.707107, 0.707107, -3.09086e-08, 0.183517, 0, -0.0534939)
@ -77,7 +72,3 @@ shadow_enabled = true
[node name="XRSimulator" parent="." instance=ExtResource("5_3qc8g")] [node name="XRSimulator" parent="." instance=ExtResource("5_3qc8g")]
xr_origin = NodePath("../XROrigin3D") xr_origin = NodePath("../XROrigin3D")
[node name="Switch" parent="." instance=ExtResource("8_uxmrb")]
transform = Transform3D(0.999999, -1.39635e-11, 0, 9.48031e-12, 1, 0, 0, 0, 1, 0.564168, 0.725642, -1.56163)
entity_id = "switch.plug_printer_2_fale"

View File

@ -1,23 +0,0 @@
extends Node3D
@onready var _controller := XRHelpers.get_xr_controller(self)
@export var light: StaticBody3D
# Called when the node enters the scene tree for the first time.
func _ready():
_controller.button_pressed.connect(self._on_button_pressed)
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
pass
func _on_button_pressed(button):
print("right: ", button)
if button != "trigger_click":
return
if light == null:
return
# set light position to controller position
light.transform.origin = _controller.transform.origin

View File

@ -12,7 +12,7 @@ var devices
var selected_device = null var selected_device = null
# Called when the node enters the scene tree for the first time. # Called when the node enters the scene tree for the first time.
func _ready(): func _ready():
devices = await HomeAdapters.adapter.load_devices() devices = await HomeAdapters.adapter.get_devices()
render_devices() render_devices()
func render_devices(): func render_devices():

View File

@ -1,11 +1,11 @@
extends Node extends Node
var Adapter = preload("res://lib/home_adapters/adapter.gd") const Adapter = preload("res://lib/home_adapters/adapter.gd")
var adapter = Adapter.new(Adapter.ADAPTER_TYPES.HASS) var adapter = Adapter.new(Adapter.ADAPTER_TYPES.HASS_WS)
var adapter_ws = Adapter.new(Adapter.ADAPTER_TYPES.HASS_WS) # var adapter_http = Adapter.new(Adapter.ADAPTER_TYPES.HASS)
func _ready(): func _ready():
add_child(adapter) add_child(adapter)
add_child(adapter_ws) # add_child(adapter_http)

View File

@ -1,7 +1,7 @@
extends Node extends Node
const hass = preload("res://lib/home_adapters/hass/hass.gd") const Hass = preload("res://lib/home_adapters/hass/hass.gd")
const hass_ws = preload("res://lib/home_adapters/hass_ws/hass.gd") const HassWebSocket = preload("res://lib/home_adapters/hass_ws/hass.gd")
enum ADAPTER_TYPES { enum ADAPTER_TYPES {
HASS, HASS,
@ -9,14 +9,16 @@ enum ADAPTER_TYPES {
} }
const adapters = { const adapters = {
ADAPTER_TYPES.HASS: hass, ADAPTER_TYPES.HASS: Hass,
ADAPTER_TYPES.HASS_WS: hass_ws ADAPTER_TYPES.HASS_WS: HassWebSocket
} }
const methods = [ const methods = [
"load_devices", "get_devices",
"get_device",
"get_state", "get_state",
"set_state" "set_state",
"watch_state"
] ]
var adapter: Node var adapter: Node
@ -28,14 +30,22 @@ func _init(type: ADAPTER_TYPES):
for method in methods: for method in methods:
assert(adapter.has_method(method), "Adapter does not implement method: " + method) assert(adapter.has_method(method), "Adapter does not implement method: " + method)
func load_devices(): ## Get a list of all devices
return await adapter.load_devices() func get_devices():
return await adapter.get_devices()
## Get a single device by id
func get_device(id: String):
return await adapter.get_device(id)
## Returns the current state of an entity
func get_state(entity: String): func get_state(entity: String):
return await adapter.get_state(entity) return await adapter.get_state(entity)
## Updates the state of the entity and returns the resulting state
func set_state(entity: String, state: String, attributes: Dictionary = {}): func set_state(entity: String, state: String, attributes: Dictionary = {}):
return await adapter.set_state(entity, state, attributes) return await adapter.set_state(entity, state, attributes)
## Watches the state and each time it changes, calls the callback with the changed state, returns a function to stop watching the state
func watch_state(entity: String, callback: Callable): func watch_state(entity: String, callback: Callable):
return adapter.watch_state(entity, callback) return adapter.watch_state(entity, callback)

View File

@ -13,7 +13,7 @@ func _init(url := self.url, token := self.token):
headers = PackedStringArray(["Authorization: Bearer %s" % token, "Content-Type: application/json"]) headers = PackedStringArray(["Authorization: Bearer %s" % token, "Content-Type: application/json"])
devices_template = devices_template.replace("\n", " ").replace("\t", "").replace("\r", " ").replace("\"", "\\\"") devices_template = devices_template.replace("\n", " ").replace("\t", "").replace("\r", " ").replace("\"", "\\\"")
func load_devices(): func get_devices():
Request.request("%s/api/template" % [url], headers, HTTPClient.METHOD_POST, "{\"template\": \"%s\"}" % [devices_template]) Request.request("%s/api/template" % [url], headers, HTTPClient.METHOD_POST, "{\"template\": \"%s\"}" % [devices_template])
var response = await Request.request_completed var response = await Request.request_completed
var data_string = response[3].get_string_from_utf8().replace("'", "\"") var data_string = response[3].get_string_from_utf8().replace("'", "\"")

View File

@ -1,13 +1,13 @@
extends Node extends Node
var devices_template := FileAccess.get_file_as_string("res://lib/home_adapters/hass/templates/devices.j2") var devices_template := FileAccess.get_file_as_string("res://lib/home_adapters/hass_ws/templates/devices.j2")
var socket := WebSocketPeer.new() var socket := WebSocketPeer.new()
# in seconds # in seconds
var request_timeout := 10.0 var request_timeout := 10.0
var url := "ws://192.168.33.33:8123/api/websocket" var url := "ws://192.168.33.33:8123/api/websocket"
var token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIzZjQ0ZGM2N2Y3YzY0MDc1OGZlMWI2ZjJlNmIxZjRkNSIsImlhdCI6MTY5ODAxMDcyOCwiZXhwIjoyMDEzMzcwNzI4fQ.K6ydLUC-4Q7BNIRCU1nWlI2s6sg9UCiOu-Lpedw2zJc" var token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIzZjQ0ZGM2N2Y3YzY0MDc1OGZlMWI2ZjJlNmIxZjRkNSIsImlhdCI6MTY5ODAxMDcyOCwiZXhwIjoyMDEzMzcwNzI4fQ.K6ydLUC-4Q7BNIRCU1nWlI2s6sg9UCiOu-Lpedw2zJc"
var LOG_MESSAGES := false var LOG_MESSAGES := true
var authenticated := false var authenticated := false
var loading := true var loading := true
@ -24,7 +24,7 @@ func _init(url := self.url, token := self.token):
self.url = url self.url = url
self.token = token self.token = token
devices_template = devices_template.replace("\n", " ").replace("\t", "").replace("\r", " ").replace("\"", "\\\"") devices_template = devices_template.replace("\n", " ").replace("\t", "").replace("\r", " ")
connect_ws() connect_ws()
func connect_ws(): func connect_ws():
@ -56,7 +56,7 @@ func _process(delta):
handle_disconnect() handle_disconnect()
func handle_packet(packet: Dictionary): func handle_packet(packet: Dictionary):
if LOG_MESSAGES: print("Received packet: %s" % packet) if LOG_MESSAGES: print("Received packet: %s" % str(packet).substr(0, 1000))
if packet.type == "auth_required": if packet.type == "auth_required":
send_packet({ send_packet({
@ -139,13 +139,23 @@ func send_subscribe_packet(packet: Dictionary, callback: Callable):
id += 1 id += 1
func send_request_packet(packet: Dictionary): func send_request_packet(packet: Dictionary, ignore_initial := false):
packet.id = id packet.id = id
id += 1 id += 1
send_packet(packet) send_packet(packet)
var promise = Promise.new(func(resolve: Callable, reject: Callable): var promise = Promise.new(func(resolve: Callable, reject: Callable):
var fn: Callable
if ignore_initial:
fn = func(packet: Dictionary):
if packet.type == "event":
resolve.call(packet)
packet_callbacks.remove(packet.id, fn)
packet_callbacks.add(packet.id, fn)
else:
packet_callbacks.add_once(packet.id, resolve) packet_callbacks.add_once(packet.id, resolve)
var timeout = Timer.new() var timeout = Timer.new()
@ -153,6 +163,9 @@ func send_request_packet(packet: Dictionary):
timeout.set_one_shot(true) timeout.set_one_shot(true)
timeout.timeout.connect(func(): timeout.timeout.connect(func():
reject.call(Promise.Rejection.new("Request timed out")) reject.call(Promise.Rejection.new("Request timed out"))
if ignore_initial:
packet_callbacks.remove(packet.id, fn)
else:
packet_callbacks.remove(packet.id, resolve) packet_callbacks.remove(packet.id, resolve)
) )
add_child(timeout) add_child(timeout)
@ -163,7 +176,7 @@ func send_request_packet(packet: Dictionary):
func send_packet(packet: Dictionary): func send_packet(packet: Dictionary):
if LOG_MESSAGES || true: print("Sending packet: %s" % encode_packet(packet)) if LOG_MESSAGES: print("Sending packet: %s" % encode_packet(packet))
socket.send_text(encode_packet(packet)) socket.send_text(encode_packet(packet))
func decode_packet(packet: PackedByteArray): func decode_packet(packet: PackedByteArray):
@ -172,11 +185,21 @@ func decode_packet(packet: PackedByteArray):
func encode_packet(packet: Dictionary): func encode_packet(packet: Dictionary):
return JSON.stringify(packet) return JSON.stringify(packet)
func load_devices(): func get_devices():
if loading: if loading:
await on_connect await on_connect
return entities var result = await send_request_packet({
"type": "render_template",
"template": devices_template,
"timeout": 3,
"report_errors": true
}, true)
return result.payload.event.result
func get_device(id: String):
pass
func get_state(entity: String): func get_state(entity: String):
if loading: if loading:
@ -193,6 +216,9 @@ func watch_state(entity: String, callback: Callable):
entitiy_callbacks.add(entity, callback) 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 = {}):
assert(!loading, "Still loading") assert(!loading, "Still loading")
@ -211,6 +237,9 @@ func set_state(entity: String, state: String, attributes: Dictionary = {}):
elif state == 'off': elif state == 'off':
service = 'turn_off' service = 'turn_off'
if service == null:
return null
return await send_request_packet({ return await send_request_packet({
"type": "call_service", "type": "call_service",
"domain": domain, "domain": domain,

View File

@ -7,6 +7,4 @@
{%- set ns.devices = ns.devices + [ {device: {"name": device_attr(device, "name"), "entities": entities }} ] %} {%- set ns.devices = ns.devices + [ {device: {"name": device_attr(device, "name"), "entities": entities }} ] %}
{%- endif %} {%- endif %}
{%- endfor %} {%- endfor %}
{ {{ ns.devices }}
"data": {{ ns.devices }}
}