rework to HomeApi and fix device loading

This commit is contained in:
Nitwel 2023-11-24 01:36:31 +01:00
parent e1f9969986
commit eb327121b8
22 changed files with 212 additions and 134 deletions

View File

@ -36,7 +36,7 @@ In order to contribute to this project, you need the following to be setup befor
## Fundamentals
Communication with the Smart Home Environment is done using the `HomeAdapters` global. Each environment is made up of devices and entities.
Communication with the Smart Home Environment is done using the `HomeApi` global. Each environment is made up of devices and entities.
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.
@ -55,9 +55,9 @@ For example, the entity of name `lights.smart_lamp_1` would control the kitchen
└── home_adapters (Code allowing control smart home entities)
```
### Home Adapters
### Home Api
The `HomeAdapters` global allows to communicate with different backends and offers a set of fundamental functions allowing communication with the Smart Home.
The `HomeApi` global allows to communicate with different backends and offers a set of fundamental functions allowing communication with the Smart Home.
```python
Device {

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:053d1e42a72e988fad4b2ef981eb72ec850f1f7da6963b257c8935e8392cb37b
size 324

View File

@ -0,0 +1,39 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://co5fgm68t4j6o"
path.s3tc="res://.godot/imported/wifi_white_24dp.svg-9edf937c7c00e607b2e1a7211dd6ea49.s3tc.ctex"
path.etc2="res://.godot/imported/wifi_white_24dp.svg-9edf937c7c00e607b2e1a7211dd6ea49.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
[deps]
source_file="res://assets/icons/wifi_white_24dp.svg"
dest_files=["res://.godot/imported/wifi_white_24dp.svg-9edf937c7c00e607b2e1a7211dd6ea49.s3tc.ctex", "res://.godot/imported/wifi_white_24dp.svg-9edf937c7c00e607b2e1a7211dd6ea49.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0
svg/scale=8.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@ -13,10 +13,10 @@ var brightness = 0 # 0-255
# 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 HomeApi.get_state(entity_id)
set_state(stateInfo["state"] == "on")
await HomeAdapters.adapter.watch_state(entity_id, func(new_state):
await HomeApi.watch_state(entity_id, func(new_state):
if (new_state["state"] == "on") == state:
return
set_state(new_state["state"] == "on")
@ -44,7 +44,7 @@ func _on_click(event):
if !state && brightness != null:
attributes["brightness"] = int(brightness)
HomeAdapters.adapter.set_state(entity_id, "on" if !state else "off", attributes)
HomeApi.set_state(entity_id, "on" if !state else "off", attributes)
set_state(!state, brightness)
else:
_on_clickable_on_click(event)
@ -71,5 +71,5 @@ func _on_clickable_on_click(event):
slider_knob.position = new_pos
HomeAdapters.adapter.set_state(entity_id, "on" if state else "off", {"brightness": int(ratio * 255)})
HomeApi.set_state(entity_id, "on" if state else "off", {"brightness": int(ratio * 255)})
set_state(state, ratio * 255)

View File

@ -5,10 +5,10 @@ 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 HomeApi.get_state(entity_id)
set_text(stateInfo)
await HomeAdapters.adapter.watch_state(entity_id, func(new_state):
await HomeApi.watch_state(entity_id, func(new_state):
set_text(new_state)
)

View File

@ -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 HomeApi.get_state(entity_id)
if stateInfo == null:
return
@ -14,7 +14,7 @@ func _ready():
else:
sprite.set_frame(1)
await HomeAdapters.adapter.watch_state(entity_id, func(new_state):
await HomeApi.watch_state(entity_id, func(new_state):
if new_state["state"] == "on":
sprite.set_frame(0)
else:
@ -23,7 +23,7 @@ func _ready():
func _on_click(event):
HomeAdapters.adapter.set_state(entity_id, "off" if sprite.get_frame() == 0 else "on")
HomeApi.set_state(entity_id, "off" if sprite.get_frame() == 0 else "on")
if sprite.get_frame() == 0:
sprite.set_frame(1)
else:

View File

@ -84,11 +84,11 @@ func _on_button_released(button: String):
last_collided = null
moved = false
func _emit_event(type: String, target: Object):
func _emit_event(type: String, target):
var event = EventRay.new()
event.controller = controller
event.target = target
event.ray = self
event.is_right_controller = is_right
EventSystem.emit(type, event)
EventSystem.emit(type, event)

View File

@ -19,7 +19,8 @@ var text_handler = preload("res://content/ui/components/input/text_handler.gd").
get:
return text_handler.text
set(value):
text_handler.set_text(value, EventSystem.is_focused(self) == false)
var focused = Engine.is_editor_hint() == false && EventSystem.is_focused(self) == false
text_handler.set_text(value, focused)
if label != null:
label.text = text_handler.get_display_text()

View File

@ -5,11 +5,11 @@
[sub_resource type="BoxMesh" id="BoxMesh_kjbca"]
resource_local_to_scene = true
size = Vector3(0.15, 0.006, 0.03)
size = Vector3(0.2, 0.006, 0.03)
[sub_resource type="BoxShape3D" id="BoxShape3D_x4yp8"]
resource_local_to_scene = true
size = Vector3(0.15, 0.006, 0.03)
size = Vector3(0.2, 0.006, 0.03)
[sub_resource type="SystemFont" id="SystemFont_nbea0"]
@ -55,7 +55,7 @@ _data = {
[node name="Input" type="StaticBody3D" groups=["ui_focus"]]
script = ExtResource("1_uml3t")
text = "Hello World"
width = 0.2
[node name="Box" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.003, 0)
@ -67,7 +67,7 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.003, 0)
shape = SubResource("BoxShape3D_x4yp8")
[node name="Label" type="Label3D" parent="."]
transform = Transform3D(1, 0, 0, 0, -4.37114e-08, 1, 0, -1, -4.37114e-08, -0.073, 0.00618291, 0)
transform = Transform3D(1, 0, 0, 0, -4.37114e-08, 1, 0, -1, -4.37114e-08, -0.098, 0.00618291, 0)
pixel_size = 0.0004
text = "Hello World"
font = SubResource("SystemFont_nbea0")

View File

@ -19,11 +19,6 @@ var pages = 0
var selected_device = null
# Called when the node enters the scene tree for the first time.
func _ready():
HomeAdapters.adapter.adapter.on_connect.connect(func():
devices = await HomeAdapters.adapter.get_devices()
render()
)
next_page_button.get_node("Clickable").on_click.connect(func(_event):
print("next page")
next_page()
@ -33,6 +28,26 @@ func _ready():
previous_page()
)
func _enter_tree():
if HomeApi.has_connected():
load_devices()
else:
HomeApi.on_connect.connect(func():
if is_inside_tree():
load_devices()
)
func load_devices():
if devices.size() == 0:
devices = await HomeApi.get_devices()
render()
HomeApi.on_disconnect.connect(func():
devices = []
if is_inside_tree():
render()
)
func update_pages():
if selected_device == null:
pages = ceil(float(devices.size()) / page_size)

View File

@ -8,7 +8,7 @@ radius = 0.08
height = 0.16
[node name="Ball" type="RigidBody3D"]
angular_damp = 39.224
angular_damp = 4.0
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
shape = SubResource("SphereShape3D_orlq6")

View File

@ -25,22 +25,23 @@ func _ready():
input_token.text = config["token"]
button_connect.on_button_down.connect(func():
HomeAdapters.adapter.adapter.url = input_url.text + "/api/websocket"
HomeAdapters.adapter.adapter.token = input_token.text
HomeAdapters.adapter.adapter.retries = 5
HomeAdapters.adapter.adapter.connect_ws()
var url = input_url.text + "/api/websocket"
var token = input_token.text
HomeApi.start_adapter("hass_ws", url, token)
ConfigData.save_config({
"api_type": "hass_ws",
"url": input_url.text,
"token": input_token.text
})
)
HomeAdapters.adapter.adapter.on_connect.connect(func():
HomeApi.on_connect.connect(func():
connection_status.text = "Connected"
)
HomeAdapters.adapter.adapter.on_disconnect.connect(func():
HomeApi.on_disconnect.connect(func():
connection_status.text = "Disconnected"
)

View File

@ -1,9 +1,10 @@
[gd_scene load_steps=6 format=3 uid="uid://c6r4higceibif"]
[gd_scene load_steps=7 format=3 uid="uid://c6r4higceibif"]
[ext_resource type="Script" path="res://content/ui/menu/settings/settings_menu.gd" id="1_0lte6"]
[ext_resource type="PackedScene" uid="uid://bsjqdvkt0u87c" path="res://content/ui/components/button/button.tscn" id="1_faxng"]
[ext_resource type="Script" path="res://content/functions/clickable.gd" id="3_qmg6q"]
[ext_resource type="PackedScene" uid="uid://blrhy2uccrdn4" path="res://content/ui/components/input/input.tscn" id="4_q3x6k"]
[ext_resource type="Texture2D" uid="uid://co5fgm68t4j6o" path="res://assets/icons/wifi_white_24dp.svg" id="5_muw54"]
[sub_resource type="BoxMesh" id="BoxMesh_e51x8"]
size = Vector3(0.3, 0.01, 0.3)
@ -30,40 +31,45 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.0458097, 0, 0.253575)
script = ExtResource("3_qmg6q")
[node name="ConnectionStatus" type="Label3D" parent="Content"]
transform = Transform3D(1, 0, 0, 0, -4.37114e-08, 1, 0, -1, -4.37114e-08, 0.26, 0, 0.29)
transform = Transform3D(1, 0, 0, 0, -4.37114e-08, 1, 0, -1, -4.37114e-08, 0.250698, 0, 0.161303)
pixel_size = 0.0003
text = "Disconnected"
[node name="LabelURL" type="Label3D" parent="Content"]
transform = Transform3D(1, 0, 0, 0, -4.37114e-08, 1, 0, -1, -4.37114e-08, 0.01, 0, 0.03)
pixel_size = 0.0005
text = "URL"
text = "url:
"
font_size = 36
horizontal_alignment = 0
[node name="InputURL" parent="Content" instance=ExtResource("4_q3x6k")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.18, 0, 0.03)
width = 0.2
text = ""
text = "ws://192.168.33.33:8123"
[node name="LabelToken" type="Label3D" parent="Content"]
transform = Transform3D(1, 0, 0, 0, -4.37114e-08, 1, 0, -1, -4.37114e-08, 0.01, 0, 0.07)
pixel_size = 0.0005
text = "TOKEN"
text = "token:"
font_size = 36
horizontal_alignment = 0
[node name="InputToken" parent="Content" instance=ExtResource("4_q3x6k")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.18, 0, 0.07)
width = 0.2
text = ""
text = "..."
[node name="LabelConnect" type="Label3D" parent="Content"]
transform = Transform3D(1, 0, 0, 0, -4.37114e-08, 1, 0, -1, -4.37114e-08, 0.15, 0, 0.12)
transform = Transform3D(1, 0, 0, 0, -4.37114e-08, 1, 0, -1, -4.37114e-08, 0.14, 0, 0.12)
pixel_size = 0.0005
text = "TOKEN"
text = "Connect"
font_size = 36
horizontal_alignment = 0
[node name="Connect" parent="Content" instance=ExtResource("1_faxng")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.25, 0, 0.12)
[node name="Sprite3D" type="Sprite3D" parent="Content"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.25, 0.012, 0.12)
pixel_size = 0.0002
axis = 1
texture = ExtResource("5_muw54")

View File

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

94
lib/globals/home_api.gd Normal file
View File

@ -0,0 +1,94 @@
extends Node
const Hass = preload("res://lib/home_apis/hass/hass.gd")
const HassWebSocket = preload("res://lib/home_apis/hass_ws/hass.gd")
const apis = {
"hass": Hass,
"hass_ws": HassWebSocket
}
const methods = [
"get_devices",
"get_device",
"get_state",
"set_state",
"watch_state"
]
signal on_connect()
signal on_disconnect()
var api: Node
func _ready():
print("HomeApi ready")
var config = ConfigData.load_config()
if config.has("api_type") && config.has("url") && config.has("token"):
var type = config["api_type"]
var url = config["url"] + "/api/websocket"
var token = config["token"]
start_adapter(type, url, token)
func start_adapter(type: String, url: String, token: String):
print("Starting adapter: %s" % type)
if api != null:
api.on_connect.disconnect(_on_connect)
api.on_disconnect.disconnect(_on_disconnect)
remove_child(api)
api.queue_free()
api = null
api = apis[type].new(url, token)
add_child(api)
api.on_connect.connect(func():
on_connect.emit()
)
api.on_disconnect.connect(func():
on_disconnect.emit()
)
for method in methods:
assert(api.has_method(method), "%s Api does not implement method: %s" % [type, method])
func _on_connect():
on_connect.emit()
func _on_disconnect():
on_disconnect.emit()
func has_connected():
if api == null:
return false
return api.has_connected()
## Get a list of all devices
func get_devices():
assert(has_connected(), "Not connected")
return await api.get_devices()
## Get a single device by id
func get_device(id: String):
assert(has_connected(), "Not connected")
return await api.get_device(id)
## Returns the current state of an entity
func get_state(entity: String):
assert(has_connected(), "Not connected")
return await api.get_state(entity)
## Updates the state of the entity and returns the resulting state
func set_state(entity: String, state: String, attributes: Dictionary = {}):
assert(has_connected(), "Not connected")
return await api.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):
assert(has_connected(), "Not connected")
return api.watch_state(entity, callback)

View File

@ -1,61 +0,0 @@
extends Node
const Hass = preload("res://lib/home_adapters/hass/hass.gd")
const HassWebSocket = preload("res://lib/home_adapters/hass_ws/hass.gd")
enum ADAPTER_TYPES {
HASS,
HASS_WS
}
const adapters = {
ADAPTER_TYPES.HASS: Hass,
ADAPTER_TYPES.HASS_WS: HassWebSocket
}
const methods = [
"get_devices",
"get_device",
"get_state",
"set_state",
"watch_state"
]
var adapter: Node
func _init(type: ADAPTER_TYPES):
var url = ""
var token = ""
var config = ConfigData.load_config()
if config.has("url"):
url = config["url"] + "/api/websocket"
if config.has("token"):
token = config["token"]
adapter = adapters[type].new(url, token)
add_child(adapter)
for method in methods:
assert(adapter.has_method(method), "Adapter does not implement method: " + method)
## Get a list of all 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):
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 = {}):
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):
return adapter.watch_state(entity, callback)

View File

@ -4,7 +4,7 @@ var url: String = "http://192.168.33.33:8123"
var token: String = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIzZjQ0ZGM2N2Y3YzY0MDc1OGZlMWI2ZjJlNmIxZjRkNSIsImlhdCI6MTY5ODAxMDcyOCwiZXhwIjoyMDEzMzcwNzI4fQ.K6ydLUC-4Q7BNIRCU1nWlI2s6sg9UCiOu-Lpedw2zJc"
var headers: PackedStringArray = PackedStringArray([])
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_apis/hass/templates/devices.j2")
func _init(url := self.url, token := self.token):
self.url = url
@ -32,8 +32,6 @@ func get_state(entity: String):
return json
func set_state(entity: String, state: String, attributes: Dictionary = {}):
var type = entity.split('.')[0]
var response

View File

@ -1,6 +1,10 @@
extends Node
var devices_template := FileAccess.get_file_as_string("res://lib/home_adapters/hass_ws/templates/devices.j2")
signal on_connect()
signal on_disconnect()
var connected := false
var devices_template := FileAccess.get_file_as_string("res://lib/home_apis/hass_ws/templates/devices.j2")
var socket := WebSocketPeer.new()
# in seconds
var request_timeout := 10.0
@ -13,7 +17,7 @@ var token := ""
var LOG_MESSAGES := false
var authenticated := false
var loading := true
var id := 1
var entities: Dictionary = {}
var retries := 5
@ -21,9 +25,6 @@ var retries := 5
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
self.token = token
@ -114,7 +115,7 @@ func start_subscriptions():
"attributes": packet.event.a[entity]["a"]
}
entitiy_callbacks.call_key(entity, [entities[entity]])
loading = false
connected = true
on_connect.emit()
if packet.event.has("c"):
@ -201,10 +202,10 @@ func decode_packet(packet: PackedByteArray):
func encode_packet(packet: Dictionary):
return JSON.stringify(packet)
func get_devices():
if loading:
await on_connect
func has_connected():
return connected
func get_devices():
var result = await send_request_packet({
"type": "render_template",
"template": devices_template,
@ -218,18 +219,12 @@ func get_device(id: String):
pass
func get_state(entity: String):
if loading:
await on_connect
if entities.has(entity):
return entities[entity]
return null
func watch_state(entity: String, callback: Callable):
if loading:
await on_connect
entitiy_callbacks.add(entity, callback)
return func():
@ -237,8 +232,6 @@ func watch_state(entity: String, callback: Callable):
func set_state(entity: String, state: String, attributes: Dictionary = {}):
assert(!loading, "Still loading")
var domain = entity.split(".")[0]
var service: String

View File

@ -20,7 +20,7 @@ config/icon="res://assets/logo.png"
XRToolsUserSettings="*res://addons/godot-xr-tools/user_settings/user_settings.gd"
ConfigData="*res://lib/globals/config_data.gd"
Request="*res://lib/globals/request.gd"
HomeAdapters="*res://lib/globals/home_adapters.gd"
HomeApi="*res://lib/globals/home_api.gd"
AudioPlayer="*res://lib/globals/audio_player.gd"
EventSystem="*res://lib/globals/event_system.gd"