2024-12-07 15:22:08 +01:00
import json
import logging
2024-12-08 11:51:05 +01:00
from collections . abc import Callable
from typing import Any
2024-12-07 15:22:08 +01:00
import paho . mqtt . client as mqtt
2024-12-09 12:16:26 +01:00
from paho . mqtt . enums import CallbackAPIVersion , MQTTErrorCode
2024-12-07 15:22:08 +01:00
from . screen import Screen
from . select import Selector
logger = logging . getLogger ( __name__ )
class HAClient :
2024-12-08 10:43:31 +01:00
def __init__ (
self ,
entity : str ,
secondary_entities : list [ str ] = [ ] ,
2024-12-08 11:51:05 +01:00
mqtt_config : dict [ str , str ] = dict ( ) ,
2024-12-08 10:43:31 +01:00
) - > None :
2024-12-07 15:22:08 +01:00
self . entity = entity
self . secondary_entities = secondary_entities
2024-12-08 11:17:25 +01:00
self . config = mqtt_config
2024-12-07 15:22:08 +01:00
self . state_topic = " oin/state "
self . availability_topic = " oin/availability "
2024-12-09 12:16:26 +01:00
self . client = mqtt . Client ( CallbackAPIVersion . VERSION2 )
2024-12-08 11:17:25 +01:00
username = self . config . get ( " username " , None )
logger . debug ( f " Setting up MQTT with user < { username } > " )
2024-12-08 10:43:31 +01:00
self . client . username_pw_set (
2024-12-08 11:17:25 +01:00
username = username ,
2024-12-08 10:43:31 +01:00
password = self . config . get ( " password " , None ) ,
)
2024-12-07 15:22:08 +01:00
self . screen = Screen ( )
self . selector = Selector ( self . send_data )
@property
2024-12-09 13:25:00 +01:00
def ha_options ( self ) - > dict [ str , Any ] :
2024-12-07 15:22:08 +01:00
return {
" dev " : {
" ids " : " oin " ,
" name " : " Oin " ,
} ,
" o " : {
" name " : " Oin " ,
} ,
" availability_topic " : self . availability_topic ,
" state_topic " : self . state_topic ,
" cmps " : self . selector . ha_options ,
}
2024-12-09 12:16:26 +01:00
def connect ( self ) - > int :
2024-12-07 15:22:08 +01:00
self . client . will_set ( self . availability_topic , " offline " , retain = True )
2024-12-08 11:17:25 +01:00
host = self . config . get ( " host " )
port = self . config . get ( " port " , 1883 )
2024-12-09 12:16:26 +01:00
if host is None :
logger . error ( " Host not found in config. " )
logger . error ( f " \t { self . config } " )
return 1
if not isinstance ( port , int ) :
logger . warning ( f " Invalid port config : < { port } > ; using port 1883. " )
port = 1883
2024-12-08 11:51:05 +01:00
logger . debug ( f " Connecting to < { host } > on port < { port } >. " )
2024-12-09 12:16:26 +01:00
code = self . client . connect ( host , port )
if code != 0 :
logger . error ( f " Could not connect to host < { host } > on port < { port } >. " )
return 1
2024-12-07 15:22:08 +01:00
2024-12-09 12:16:26 +01:00
code = self . subscribe ( entity_topic ( self . entity ) , self . state_update )
if code != 0 :
return 1
code = self . subscribe (
2024-12-08 11:51:05 +01:00
[ entity_topic ( entity ) for entity in self . secondary_entities ] ,
self . secondary_state_update ,
)
2024-12-09 12:16:26 +01:00
if code != 0 :
return 1
2024-12-07 15:22:08 +01:00
2024-12-09 12:16:26 +01:00
m_info = self . publish_json (
" homeassistant/device/oin/config " , self . ha_options , retain = True
)
m_info . wait_for_publish ( 60 )
if not m_info . is_published ( ) :
logger . error ( " Config message timed out " )
return 1
m_info = self . publish ( self . availability_topic , " online " , retain = True )
m_info . wait_for_publish ( 60 )
if not m_info . is_published ( ) :
logger . error ( " Availability message timed out " )
return 1
return 0
def publish (
self , topic : str , data : str , retain : bool = False
) - > mqtt . MQTTMessageInfo :
logger . debug ( f " Sending message on topic < { topic } >. " )
return self . client . publish ( topic , data , retain = retain )
def publish_json (
self , topic : str , data : Any , retain : bool = False
) - > mqtt . MQTTMessageInfo :
return self . publish ( topic , json . dumps ( data ) , retain )
def subscribe (
self ,
topic : str | list [ str ] ,
callback : Callable [ [ mqtt . Client , Any , mqtt . MQTTMessage ] , None ] ,
) - > MQTTErrorCode :
2024-12-08 11:51:05 +01:00
logger . debug ( f " Subscribing to < { topic } >. " )
match topic :
case str ( ) :
self . client . message_callback_add ( topic , callback )
code , _ = self . client . subscribe ( topic )
case list ( ) :
for top in topic :
self . client . message_callback_add ( top , callback )
code , _ = self . client . subscribe ( [ ( top , 0 ) for top in topic ] )
2024-12-07 15:22:08 +01:00
2024-12-08 11:51:05 +01:00
if code != 0 :
logger . error ( f " Failed subscribing to topic < { topic } > with code < { code } >. " )
2024-12-09 12:16:26 +01:00
return code
2024-12-07 15:22:08 +01:00
2024-12-09 12:16:26 +01:00
def loop ( self ) - > MQTTErrorCode :
2024-12-08 11:51:05 +01:00
logger . info ( " Starting MQTT client loop. " )
code = self . client . loop_forever ( retry_first_connection = True )
2024-12-07 15:22:08 +01:00
2024-12-08 11:51:05 +01:00
if code != 0 :
logger . error ( " MQTT client loop failed with code < {code} >. " )
2024-12-09 12:16:26 +01:00
return code
2024-12-07 15:22:08 +01:00
2024-12-08 11:51:05 +01:00
def state_update (
self , client : mqtt . Client , userdata : Any , message : mqtt . MQTTMessage
) - > None :
2024-12-09 12:16:26 +01:00
data = message . payload . decode ( )
logger . debug ( f " Message received on topic < { message . topic } >: { data } . " )
2024-12-07 15:22:08 +01:00
subtopic = message . topic . rsplit ( " / " , maxsplit = 1 ) [ 1 ]
match subtopic :
case " current_temperature " :
2024-12-09 12:16:26 +01:00
self . screen . value = json . loads ( data )
2024-12-07 15:22:08 +01:00
case " temperature " :
2024-12-09 12:16:26 +01:00
if ( value := json . loads ( data ) ) != self . selector . temperature :
2024-12-07 15:22:08 +01:00
self . screen . tmp_value = value
self . selector . temperature = value
case " hvac_action " :
2024-12-09 12:16:26 +01:00
self . screen . mode = json . loads ( data )
2024-12-07 15:22:08 +01:00
case " preset_modes " :
2024-12-09 12:16:26 +01:00
if ( value := json . loads ( data ) ) != self . selector . preset_modes :
2024-12-07 15:22:08 +01:00
self . selector . preset_modes = value
case " preset_mode " :
2024-12-09 12:16:26 +01:00
if ( value := json . loads ( data ) ) != self . selector . mode :
2024-12-07 15:22:08 +01:00
self . selector . mode = value
case " state " :
2024-12-09 12:16:26 +01:00
match data :
2024-12-07 15:22:08 +01:00
case " heat " :
self . selector . switch = True
case " off " :
self . selector . switch = False
2024-12-09 13:25:00 +01:00
case other :
logger . warning ( f " Unknown state received: < { other } >. " )
case _ :
pass
2024-12-07 15:22:08 +01:00
def secondary_state_update (
2024-12-08 11:51:05 +01:00
self , client : mqtt . Client , userdata : Any , message : mqtt . MQTTMessage
) - > None :
2024-12-09 12:16:26 +01:00
data = message . payload . decode ( )
logger . debug ( f " Message received on topic < { message . topic } >: { data } . " )
2024-12-07 15:22:08 +01:00
_ , grp , ent , subtopic = message . topic . split ( " / " )
idx = self . secondary_entities . index ( f " { grp } . { ent } " )
if subtopic == " state " :
2024-12-09 12:16:26 +01:00
self . screen . secondary | = { idx : data }
2024-12-07 15:22:08 +01:00
2024-12-08 11:51:05 +01:00
def send_data ( self , data : Any ) - > mqtt . MQTTMessageInfo :
2024-12-09 12:16:26 +01:00
return self . publish_json ( self . state_topic , data )
2024-12-07 15:22:08 +01:00
2024-12-08 11:51:05 +01:00
def entity_topic ( entity : str , subtopic : str = " # " ) - > str :
2024-12-07 15:22:08 +01:00
topic = entity . replace ( " . " , " / " )
return f " homeassistant/ { topic } / { subtopic } "