diff --git a/apps/aircon/src/aircon_device.erl b/apps/aircon/src/aircon_device.erl new file mode 100644 index 0000000..684e93d --- /dev/null +++ b/apps/aircon/src/aircon_device.erl @@ -0,0 +1,177 @@ +%%%------------------------------------------------------------------- +%%% @author anlicheng +%%% @copyright (C) 2024, +%%% @doc +%%% +%%% @end +%%% Created : 04. 9月 2024 16:13 +%%%------------------------------------------------------------------- +-module(aircon_device). +-author("anlicheng"). + +-behaviour(gen_server). + +%% API +-export([start_link/2]). +-export([get_pid/1, get_name/1]). +-export([metric_data/2, poll_status/1]). + +-define(UNIT0, 16#10). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). + +-record(state, { + device_uuid :: binary(), + heartbeat_ticker :: integer(), + %% 消息计数器 + data_counter = 0, + %% 状态,1: 在线,0: 离线 + status = 0 +}). + +%%%=================================================================== +%%% API +%%%=================================================================== + +-spec get_pid(DeviceUUID :: binary()) -> undefined | pid(). +get_pid(DeviceUUID) when is_binary(DeviceUUID) -> + whereis(get_name(DeviceUUID)). + +-spec get_name(DeviceUUID :: binary()) -> atom(). +get_name(DeviceUUID) when is_binary(DeviceUUID) -> + binary_to_atom(<<"aircon_device:", DeviceUUID/binary>>). + +-spec metric_data(Pid :: pid(), Message :: binary()) -> no_return(). +metric_data(Pid, Message) when is_pid(Pid), is_binary(Message) -> + gen_server:cast(Pid, {metric_data, Message}). + +%% 查询设备的当前状态, 1: 在线,0: 离线 +-spec poll_status(Pid :: pid()) -> {ok, Status :: integer()}. +poll_status(Pid) when is_pid(Pid) -> + gen_server:call(Pid, poll_status). + +%% @doc Spawns the server and registers the local name (unique) +-spec(start_link(Name :: atom(), DeviceUUID :: binary()) -> + {ok, Pid :: pid()} | ignore | {error, Reason :: term()}). +start_link(Name, DeviceUUID) when is_atom(Name), is_binary(DeviceUUID) -> + gen_server:start_link({local, get_name(DeviceUUID)}, ?MODULE, [DeviceUUID], []). + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== + +%% @private +%% @doc Initializes the server +-spec(init(Args :: term()) -> + {ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} | + {stop, Reason :: term()} | ignore). +init([DeviceUUID]) -> + {ok, HeartbeatTicker0} = application:get_env(aircon, heartbeat_ticker), + HeartbeatTicker = HeartbeatTicker0 * 1000, + + erlang:start_timer(HeartbeatTicker, self(), heartbeat_ticker), + {ok, #state{device_uuid = DeviceUUID, heartbeat_ticker = HeartbeatTicker, status = 0}}. + +%% @private +%% @doc Handling call messages +-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()}, + State :: #state{}) -> + {reply, Reply :: term(), NewState :: #state{}} | + {reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} | + {noreply, NewState :: #state{}} | + {noreply, NewState :: #state{}, timeout() | hibernate} | + {stop, Reason :: term(), Reply :: term(), NewState :: #state{}} | + {stop, Reason :: term(), NewState :: #state{}}). +handle_call(poll_status, _From, State = #state{status = Status}) -> + {reply, {ok, Status}, State}; +handle_call(_Request, _From, State = #state{}) -> + {reply, ok, State}. + +%% @private +%% @doc Handling cast messages +-spec(handle_cast(Request :: term(), State :: #state{}) -> + {noreply, NewState :: #state{}} | + {noreply, NewState :: #state{}, timeout() | hibernate} | + {stop, Reason :: term(), NewState :: #state{}}). +handle_cast({metric_data, Message}, State = #state{device_uuid = DeviceUUID, data_counter = DataCounter, status = Status}) -> + case catch jiffy:decode(Message, [return_maps]) of + #{<<"properties">> := Props0} -> + Props = lists:map(fun(Fields) -> transform(Fields#{<<"device_uuid">> => DeviceUUID}) end, Props0), + Info = iolist_to_binary(jiffy:encode(Props, [force_utf8])), + case catch efka_client:send_metric_data(Props, #{}) of + {ok, _} -> + aircon_logger:write([<<"OK">>, Info]); + _ -> + aircon_logger:write([<<"ERROR">>, Info]) + end, + + %% 如果设备当前是离线状态,则需要发送上线消息 + Status == 0 andalso efka_client:device_online(DeviceUUID), + + {noreply, State#state{data_counter = DataCounter + 1, status = 1}}; + M when is_map(M) -> + lager:notice("[power_device] invalid map: ~p", [M]); + Error -> + lager:notice("[power_device] jiffy decode error: ~p", [Error]), + {noreply, State} + end. + +%% @private +%% @doc Handling all non call/cast messages +-spec(handle_info(Info :: timeout() | term(), State :: #state{}) -> + {noreply, NewState :: #state{}} | + {noreply, NewState :: #state{}, timeout() | hibernate} | + {stop, Reason :: term(), NewState :: #state{}}). +handle_info({timeout, _, heartbeat_ticker}, State = #state{device_uuid = DeviceUUID, heartbeat_ticker = HeartbeatTicker, data_counter = DataCounter, status = Status}) -> + erlang:start_timer(HeartbeatTicker, self(), heartbeat_ticker), + case DataCounter > 0 of + true -> + {noreply, State}; + false -> + Status == 1 andalso efka_client:device_offline(DeviceUUID), + {noreply, State#state{status = 0}} + end. + +%% @private +%% @doc This function is called by a gen_server when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any +%% necessary cleaning up. When it returns, the gen_server terminates +%% with Reason. The return value is ignored. +-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()), + State :: #state{}) -> term()). +terminate(_Reason, _State = #state{}) -> + ok. + +%% @private +%% @doc Convert process state when code is changed +-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{}, + Extra :: term()) -> + {ok, NewState :: #state{}} | {error, Reason :: term()}). +code_change(_OldVsn, State = #state{}, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== + +%% 网关上来的数据写反了: value是类型,unit才是数值 +%% [{"value":"int","unit":44081,"type":"AI","timestamp":1730799797,"name":"使用次数","label":"次","key":"use_times","device_uuid":"30409239002349977617259608405456"}] +-spec transform(Prop :: map()) -> map(). +transform(Prop = #{<<"key">> := <<"total_power">>, <<"unit">> := Unit}) -> + Prop#{<<"name">> => <<"总能耗"/utf8>>, <<"value">> => Unit, <<"unit">> => 16#02}; +transform(Prop = #{<<"key">> := <<"total_runtime">>, <<"unit">> := Unit}) -> + Prop#{<<"name">> => <<"总运行时间"/utf8>>, <<"value">> => Unit, <<"unit">> => ?UNIT0}; +transform(Prop = #{<<"key">> := <<"use_times">>, <<"unit">> := Unit}) -> + Prop#{<<"name">> => <<"使用次数"/utf8>>, <<"label">> => <<"次"/utf8>>, <<"value">> => Unit, <<"unit">> => ?UNIT0}; +transform(Prop = #{<<"key">> := <<"light_switch">>, <<"unit">> := Unit}) -> + Prop#{<<"name">> => <<"开关"/utf8>>, <<"value">> => Unit, <<"unit">> => ?UNIT0}; +transform(Prop = #{<<"key">> := <<"light_brightness">>, <<"unit">> := Unit}) when is_integer(Unit) -> + Label = iolist_to_binary([<<"亮度设置为:"/utf8>>, integer_to_binary(Unit)]), + Prop#{<<"name">> => <<"亮度"/utf8>>, <<"label">> => Label, <<"value">> => Unit, <<"unit">> => ?UNIT0}; +transform(Prop = #{<<"key">> := <<"light_change_time">>, <<"unit">> := Unit}) -> + Prop#{<<"name">> => <<"变亮变暗时间"/utf8>>, <<"value">> => Unit, <<"unit">> => ?UNIT0}; +transform(Prop = #{<<"key">> := <<"light_status">>, <<"unit">> := Unit}) -> + Prop#{<<"name">> => <<"是否损坏"/utf8>>, <<"value">> => Unit, <<"unit">> => ?UNIT0}; +transform(Prop = #{<<"unit">> := Unit}) -> + Prop#{<<"value">> => Unit, <<"unit">> => ?UNIT0}. \ No newline at end of file diff --git a/apps/aircon/src/aircon_device_sup.erl b/apps/aircon/src/aircon_device_sup.erl new file mode 100644 index 0000000..ec47362 --- /dev/null +++ b/apps/aircon/src/aircon_device_sup.erl @@ -0,0 +1,80 @@ +%%%------------------------------------------------------------------- +%%% @author anlicheng +%%% @copyright (C) 2024, +%%% @doc +%%% +%%% @end +%%% Created : 04. 9月 2024 15:39 +%%%------------------------------------------------------------------- +-module(aircon_device_sup). +-author("anlicheng"). + +-behaviour(supervisor). + +%% API +-export([start_link/0]). +-export([ensure_device_started/1, delete_device/1]). + +%% Supervisor callbacks +-export([init/1]). + +-define(SERVER, ?MODULE). + +%%%=================================================================== +%%% API functions +%%%=================================================================== + +%% @doc Starts the supervisor +-spec(start_link() -> {ok, Pid :: pid()} | ignore | {error, Reason :: term()}). +start_link() -> + supervisor:start_link({local, ?SERVER}, ?MODULE, []). + +%%%=================================================================== +%%% Supervisor callbacks +%%%=================================================================== + +%% @private +%% @doc Whenever a supervisor is started using supervisor:start_link/[2,3], +%% this function is called by the new process to find out about +%% restart strategy, maximum restart frequency and child +%% specifications. +init([]) -> + SupFlags = #{strategy => one_for_one, intensity => 1000, period => 3600}, + + {ok, {SupFlags, []}}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== + +-spec ensure_device_started(DeviceUUID :: binary()) -> {ok, Pid :: pid()} | {error, Reason :: any()}. +ensure_device_started(DeviceUUID) when is_binary(DeviceUUID) -> + case aircon_device:get_pid(DeviceUUID) of + DevicePid when is_pid(DevicePid) -> + {ok, DevicePid}; + undefined -> + case supervisor:start_child(?MODULE, child_spec(DeviceUUID)) of + {ok, Pid} when is_pid(Pid) -> + {ok, Pid}; + {error, {'already_started', Pid}} when is_pid(Pid) -> + {ok, Pid}; + {error, Error} -> + {error, Error} + end + end. + +delete_device(DeviceUUID) when is_binary(DeviceUUID) -> + Id = aircon_device:get_name(DeviceUUID), + ok = supervisor:terminate_child(?MODULE, Id), + supervisor:delete_child(?MODULE, Id). + +child_spec(DeviceUUID) when is_binary(DeviceUUID) -> + Name = aircon_device:get_name(DeviceUUID), + #{ + id => Name, + start => {aircon_device, start_link, [Name, DeviceUUID]}, + restart => permanent, + shutdown => 2000, + type => worker, + modules => ['aircon_device'] + }. \ No newline at end of file diff --git a/apps/aircon/src/aircon_sup.erl b/apps/aircon/src/aircon_sup.erl index b75a62e..93fb151 100644 --- a/apps/aircon/src/aircon_sup.erl +++ b/apps/aircon/src/aircon_sup.erl @@ -63,14 +63,14 @@ init([]) -> modules => ['aircon_args'] }, - %#{ - % id => 'power_device_sup', - % start => {'power_device_sup', start_link, []}, - % restart => permanent, - % shutdown => 2000, - % type => worker, - % modules => ['power_device_sup'] - %}, + #{ + id => 'aircon_device_sup', + start => {'aircon_device_sup', start_link, []}, + restart => permanent, + shutdown => 2000, + type => worker, + modules => ['aircon_device_sup'] + }, #{ id => 'aircon_mqtt_subscriber',