Created
December 4, 2024 14:59
-
-
Save ivan4th/4a807971dc92b5a4eb39f2476910750f to your computer and use it in GitHub Desktop.
izba control system test
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(defpackage :izba | |
(:use :cl :alexandria :i4-diet-utils :iterate :cs-common :cs-io :cs-cells :cs-core :cs-mqtt)) | |
(in-package :izba) | |
(defconf *mqtt-server* "mqtt") | |
(defvar *pmc*) | |
(defvar *ds*) | |
(defvar *mhdc*) | |
(defvar *to-create* (make-hash-table)) | |
(defun running-p () | |
(cl-async-repl:event-thread-running-p)) | |
(defun get-parent-from-initargs (default-parent initargs) | |
(destructuring-bind (&key parent-name parent-class &allow-other-keys) initargs | |
(cond ((and parent-name parent-class) | |
(error "can't specify both :PARENT-NAME and :PARENT-CLASS")) | |
(parent-name | |
(object-by-name *root* parent-name)) | |
(parent-class | |
(object-by-class *root* parent-class)) | |
(t default-parent)))) | |
(defun reg (class-name &rest initargs) | |
(let ((name (or (getf initargs :name) | |
(setf (getf initargs :name) class-name)))) | |
(setf (gethash name *to-create*) (cons class-name initargs)) | |
(when (running-p) | |
(cl-async-repl::sync-action | |
#'(lambda () | |
(when-let ((existing (object-by-name *root* name nil))) | |
(remove-object existing) | |
;; TBD: this shouldn't be needed! | |
(remhash existing cs-cells::*active-cell-contexts*)) | |
(let* ((umbrella (object-by-class *root* 'timed-umbrella)) | |
(created (apply #'make-instance | |
class-name | |
:parent (get-parent-from-initargs umbrella initargs) | |
:model (cell-model-of (cs-view umbrella)) | |
:name name | |
(remove-from-plist initargs :name :parent-name :parent-class)))) | |
(dbg "Created: ~s" created) | |
(process-events *root*))))))) | |
(defun remove-auto-create (name) | |
(remhash name *to-create*)) | |
(defun time-between (current-time a b) | |
(flet ((parse-tspec (tspec) | |
(destructuring-bind (hour &optional (min 0) (sec 0)) | |
(ensure-list tspec) | |
(+ (* hour 3600) (* min 60) sec)))) | |
(let ((t-cur (multiple-value-bind (sec min hour) | |
(decode-universal-time | |
(unix-timestamp->universal-time current-time)) | |
(+ (* hour 3600) (* min 60) sec))) | |
(t-a (parse-tspec a)) | |
(t-b (parse-tspec b))) | |
(cond ((< t-a t-b) | |
;; if A is before B, then CURRENT-TIME must be between A and B. | |
;; A=B means CU | |
(<= t-a t-cur t-b)) | |
((= t-a t-b) | |
(warn "TIME-BETWEEN will not work correctly for A == B") | |
nil) | |
;; if A is after B, then CURRENT-TIME must be between A and the midnight, | |
;; or between the midnight and B | |
((or (<= t-a t-cur) | |
(<= t-cur t-b))))))) | |
(define-cell-class ticker (cell-context) | |
((tick-interval :accessor tick-interval-of | |
:initform 10d0 | |
:initarg :tick-interval)) | |
(:global-cells | |
(.real-time "Current real time"))) | |
(defmethod initialize-instance :after ((ticker ticker) &key &allow-other-keys) | |
(set-timeout ticker | |
#'(lambda () (wake-once ticker)) | |
(tick-interval-of ticker) | |
:repeat t)) | |
(defclass timed-umbrella (umbrella) | |
((ticker :accessor ticker-of))) | |
(defmethod initialize-instance :after ((umbrella timed-umbrella) | |
&key (tick-interval nil tick-interval-p) | |
(model (error "must specify :MODEL")) | |
&allow-other-keys) | |
(setf (ticker-of umbrella) | |
(apply #'make-instance 'ticker | |
:name 'ticker | |
:parent umbrella | |
:model model | |
(when tick-interval-p | |
(list :tick-interval tick-interval))))) | |
(defmethod run-object :before ((umbrella timed-umbrella)) | |
(setf (cell-value 'real-time (ticker-of umbrella)) | |
(current-real-time umbrella))) | |
(define-hass-local-device local-dev-test (hass-local-device) | |
() | |
(:desc "Local device for testing") | |
(:node-id "local-dev-test") | |
(:global-cells | |
(.testswitch1 "Test Switch 1") | |
(.testlight1 "Test Light 1") | |
(.climate-mode "Climate mode") | |
(.climate-set-temp "Climate target temperature" (:value 20d0))) | |
(:objects | |
(:switch "testswitch1" .testswitch1) | |
(:light "testlight1" .testlight1) | |
(:climate "thermostat1" | |
:modes '(:off :heat) | |
:mode-cell .climate-mode | |
:set-temperature-cell .climate-set-temp))) | |
(define-hass-local-device local-dev-test-2 (hass-local-device) | |
() | |
(:desc "Local device for testing") | |
(:node-id "local-dev-test-2") | |
(:global-cells | |
(.testswitch2 "Test Switch 2") | |
(.testlight2 "Test Light 2") | |
(.testlight3 "Test Light 3") | |
(.climate-mode2 "Climate mode 2") | |
(.climate-set-temp2 "Climate target temperature 2" (:value 21d0))) | |
(:objects | |
(:switch "testswitch2" .testswitch2) | |
(:light "testlight2" .testlight2) | |
(:light "testlight3" .testlight3) | |
(:climate "thermostat2" | |
:modes '(:off :heat) | |
:mode-cell .climate-mode2 | |
:set-temperature-cell .climate-set-temp2))) | |
(define-cell-class multi-switch (cell-fsm) | |
((cells :accessor cells-of :initarg :cells :initform (error "must specify :CELLS"))) | |
(:cells | |
(is-on (:type 'boolean) :switch))) | |
(define-dynamic-cell-method .any-active ((switch multi-switch)) | |
`(or ,@(mapcar #'make-cell-ref (cells-of switch)))) | |
(define-dynamic-cell-method .all-active ((switch multi-switch)) | |
`(and ,@(mapcar #'make-cell-ref (cells-of switch)))) | |
(define-dynamic-cell-method .set-on ((switch multi-switch) on) | |
`(setf ,@(iter (for cell in (cells-of switch)) | |
(collect (make-cell-ref cell)) | |
(collect 'on)))) | |
;; Need to have fallback cells so that the FSM is in the | |
;; :INCOMPLETE state till all of the needed cells appear | |
(defmethod context-cell-specs :around ((switch multi-switch)) | |
(append | |
(iter (for cell in (cells-of switch)) | |
(collect (list cell :value-type 'boolean :fallback t))) | |
(call-next-method))) | |
(defmethod initial-state ((switch multi-switch)) :decide) | |
(define-state/cells ((switch multi-switch) :decide (cell-wait-state)) | |
(:transitions | |
((.any-active) | |
(setf .is-on t) | |
:on) | |
((not (.any-active)) | |
(setf .is-on nil) | |
:off))) | |
(define-state/cells ((switch multi-switch) :off (cell-wait-state)) | |
(:enter | |
(.set-on nil)) | |
(:transitions | |
(.is-on :on) | |
((.any-active) | |
:off))) | |
(define-state/cells ((switch multi-switch) :on (cell-wait-state)) | |
(:enter | |
(.set-on t)) | |
(:transitions | |
((not .is-on) | |
:off) | |
((not (.all-active)) | |
:on))) | |
;; FIXME: should use a common base class | |
(define-cell-class relaxed-multi-switch (multi-switch) ()) | |
;; FIXME: rm (same as one for multi-switch) | |
#++ | |
(define-state/cells ((switch relaxed-multi-switch) :decide (cell-wait-state)) | |
(:transitions | |
((.any-active) | |
(setf .is-on t) | |
:on) | |
((not (.any-active)) | |
(setf .is-on nil) | |
:off))) | |
(define-state/cells ((switch relaxed-multi-switch) :off (cell-wait-state)) | |
(:transitions | |
(.is-on | |
(.set-on t) | |
:on) | |
((.any-active) | |
(setf .is-on t) | |
:on))) | |
(define-state/cells ((switch relaxed-multi-switch) :on (cell-wait-state)) | |
(:transitions | |
((not .is-on) | |
(.set-on nil) | |
:off) | |
((not (.any-active)) | |
(setf .is-on nil) | |
:off))) | |
(define-cell-class boiler (mqtt-remote-device) | |
() | |
(:cells | |
(temp-burner | |
"Burner temperature" | |
(:address (:mqtt :real-number "/devices/boiler/controls/temp-burner" | |
nil "/devices/boiler/avail"))) | |
(temp-burner-deriv | |
"Burner temperature time derivative" | |
(:address (:mqtt :real-number "/devices/boiler/controls/temp-deriv" | |
nil "/devices/boiler/avail"))) | |
(temp-tank-a | |
"Tank A" | |
(:address (:mqtt :real-number "/devices/boiler/controls/temp-tank-a" | |
nil "/devices/boiler/avail"))) | |
(temp-tank-b | |
"Tank B" | |
(:address (:mqtt :real-number "/devices/boiler/controls/temp-tank-b" | |
nil "/devices/boiler/avail"))) | |
(temp-tank-c | |
"Tank C" | |
(:address (:mqtt :real-number "/devices/boiler/controls/temp-tank-c" | |
nil "/devices/boiler/avail"))) | |
(temp-boiler-to-tank | |
"Boiler to tank" | |
(:address (:mqtt :real-number "/devices/boiler/controls/temp-boiler-to-tank" | |
nil "/devices/boiler/avail"))) | |
(temp-tank-to-boiler | |
"Tank to boiler" | |
(:address (:mqtt :real-number "/devices/boiler/controls/temp-tank-to-boiler" | |
nil "/devices/boiler/avail"))) | |
(temp-tank-to-house | |
"Tank to house" | |
(:address (:mqtt :real-number "/devices/boiler/controls/temp-tank-to-house" | |
nil "/devices/boiler/avail"))) | |
(temp-house-to-tank | |
"House to tank" | |
(:address (:mqtt :real-number "/devices/boiler/controls/temp-house-to-tank" | |
nil "/devices/boiler/avail"))) | |
(valve-x | |
"Valve X" | |
(:address (:mqtt :real-number "/devices/boiler/controls/valveX" | |
nil "/devices/boiler/avail"))) | |
(valve-y | |
"Valve Y" | |
(:address (:mqtt :real-number | |
"/devices/boiler/controls/valveY" | |
"/devices/boiler/controls/valveY/on" | |
"/devices/boiler/avail"))) | |
(target-temp | |
"Target radiator water temperature" | |
(:address (:mqtt :real-number | |
"/devices/boiler/controls/targetTemp" | |
"/devices/boiler/controls/targetTemp/on" | |
"/devices/boiler/avail"))) | |
(target-temp | |
"Target radiator water temperature" | |
(:address (:mqtt :real-number | |
"/devices/boiler/controls/targetTemp" | |
"/devices/boiler/controls/targetTemp/on" | |
"/devices/boiler/avail"))) | |
(radiator-circulation-relay | |
"Radiator circulation relay" | |
(:type 'boolean) | |
(:address (:mqtt :simple-switch | |
"/devices/boiler/controls/radiator-circulation-relay" | |
"/devices/boiler/controls/radiator-circulation-relay/on" | |
"/devices/boiler/avail"))) | |
(boiler-circulation-relay | |
"Boiler circulation relay" | |
(:type 'boolean) | |
(:address (:mqtt :simple-switch | |
"/devices/boiler/controls/boiler-circulation-relay" | |
"/devices/boiler/controls/boiler-circulation-relay/on" | |
"/devices/boiler/avail"))) | |
(enable-valve-control | |
(:type 'boolean) | |
(:address (:mqtt :simple-switch | |
"/devices/boiler/controls/enable-valve-control" | |
"/devices/boiler/controls/enable-valve-control/on" | |
"/devices/boiler/avail"))) | |
(radiator-1-kitchen | |
(:type 'boolean) | |
(:address (:mqtt :simple-switch | |
"/devices/boiler/controls/radiatorValve1Open" | |
"/devices/boiler/controls/radiatorValve1Open/on" | |
"/devices/boiler/avail"))) | |
(radiator-1-hall | |
(:type 'boolean) | |
(:address (:mqtt :simple-switch | |
"/devices/boiler/controls/radiatorValve2Open" | |
"/devices/boiler/controls/radiatorValve2Open/on" | |
"/devices/boiler/avail"))) | |
(radiator-1-bedroom-east | |
(:type 'boolean) | |
(:address (:mqtt :simple-switch | |
"/devices/boiler/controls/radiatorValve3Open" | |
"/devices/boiler/controls/radiatorValve3Open/on" | |
"/devices/boiler/avail"))) | |
(radiator-1-bedroom-north | |
(:type 'boolean) | |
(:address (:mqtt :simple-switch | |
"/devices/boiler/controls/radiatorValve4Open" | |
"/devices/boiler/controls/radiatorValve4Open/on" | |
"/devices/boiler/avail"))) | |
(radiator-1-lobby | |
(:type 'boolean) | |
(:address (:mqtt :simple-switch | |
"/devices/boiler/controls/radiatorValve5Open" | |
"/devices/boiler/controls/radiatorValve5Open/on" | |
"/devices/boiler/avail"))) | |
(radiator-1-dining-room-south-west | |
(:type 'boolean) | |
(:address (:mqtt :simple-switch | |
"/devices/boiler/controls/radiatorValve6Open" | |
"/devices/boiler/controls/radiatorValve6Open/on" | |
"/devices/boiler/avail"))) | |
(radiator-1-dining-room-north-west | |
(:type 'boolean) | |
(:address (:mqtt :simple-switch | |
"/devices/boiler/controls/radiatorValve7Open" | |
"/devices/boiler/controls/radiatorValve7Open/on" | |
"/devices/boiler/avail"))) | |
(radiator-2-mishas-room-south | |
(:type 'boolean) | |
(:address (:mqtt :simple-switch | |
"/devices/boiler/controls/radiatorValve8Open" | |
"/devices/boiler/controls/radiatorValve8Open/on" | |
"/devices/boiler/avail"))) | |
(radiator-2-mishas-room-west | |
(:type 'boolean) | |
(:address (:mqtt :simple-switch | |
"/devices/boiler/controls/radiatorValve9Open" | |
"/devices/boiler/controls/radiatorValve9Open/on" | |
"/devices/boiler/avail"))) | |
(radiator-2-bedroom-west | |
(:type 'boolean) | |
(:address (:mqtt :simple-switch | |
"/devices/boiler/controls/radiatorValve10Open" | |
"/devices/boiler/controls/radiatorValve10Open/on" | |
"/devices/boiler/avail"))) | |
(radiator-2-bedroom-north | |
(:type 'boolean) | |
(:address (:mqtt :simple-switch | |
"/devices/boiler/controls/radiatorValve11Open" | |
"/devices/boiler/controls/radiatorValve11Open/on" | |
"/devices/boiler/avail"))) | |
(radiator-2-homeoffice | |
(:type 'boolean) | |
(:address (:mqtt :simple-switch | |
"/devices/boiler/controls/radiatorValve12Open" | |
"/devices/boiler/controls/radiatorValve12Open/on" | |
"/devices/boiler/avail"))) | |
(radiator-2-hall | |
(:type 'boolean) | |
(:address (:mqtt :simple-switch | |
"/devices/boiler/controls/radiatorValve13Open" | |
"/devices/boiler/controls/radiatorValve13Open/on" | |
"/devices/boiler/avail"))) | |
(all-radiators-off | |
(:type 'boolean) | |
(:formula | |
(not (or .radiator-1-bedroom-north | |
.radiator-1-lobby | |
.radiator-1-hall | |
.radiator-1-dining-room-south-west | |
.radiator-1-dining-room-north-west | |
.radiator-2-mishas-room-south | |
.radiator-2-mishas-room-west | |
.radiator-2-bedroom-west | |
.radiator-2-bedroom-north | |
.radiator-2-homeoffice | |
.radiator-2-hall)))) | |
(circulation-not-needed-p | |
(:type 'boolean) | |
(:formula (and .all-radiators-off | |
(not .boiler-circulation-relay)))) | |
(temp-diff | |
(:formula (- .temp-tank-to-house .temp-house-to-tank))) | |
(temp-diff-a | |
(:formula (- .temp-tank-to-house .temp-tank-a))) | |
(mix-factor | |
(:formula (let ((a .temp-house-to-tank) | |
(b .temp-tank-a) | |
(c (+ .temp-tank-to-house 0.33d0))) | |
(if (= a b) | |
0d0 ;; avoid dividing by zero | |
(/ (- c b) (- a b))))))) | |
(:default-initargs :raw-name "boiler")) | |
(define-cell-class timed-on-off (cell-fsm) | |
((time-a :accessor time-a-of :initarg :time-a | |
:initform (error "must specify TIME-A")) | |
(time-b :accessor time-b-of :initarg :time-b | |
:initform (error "must specify TIME-B")) | |
(back-to-off-delay :accessor back-to-off-delay-of | |
:initarg :back-to-off-delay | |
:initform 1500d0)) | |
(:io | |
(.target-cell (:type 'boolean)))) | |
(defmethod initial-state ((tonoff timed-on-off)) :off) | |
(define-state/cells ((tonoff timed-on-off) :off) | |
(:enter | |
(setf .target-cell nil)) | |
(:transitions | |
((time-between .real-time (time-a-of tonoff) (time-b-of tonoff)) :on) | |
(.target-cell :temporary-on))) | |
(define-state/cells ((tonoff timed-on-off) :temporary-on) | |
(:enter | |
(fsm-timeout tonoff (back-to-off-delay-of tonoff))) | |
(:transitions | |
(:timeout :off) | |
((not .target-cell) :off))) | |
(define-state/cells ((tonoff timed-on-off) :on) | |
(:enter | |
(setf .target-cell t)) | |
(:transitions | |
((not (time-between .real-time (time-a-of tonoff) (time-b-of tonoff))) :off))) | |
;; TBD: trying to use these hass-rules causes endless loop | |
#++ | |
(define-tag-rules hass-rules | |
(:real-number => (:type 'double-float)) | |
(:temperature => (:units "degC") :real-number) | |
(:climate-mode => (:type '((0 :off) (1 :heat)))) | |
(:switch => (:type 'boolean))) | |
;; TBD: MODE should be replaced with ACTIVE (boolean) | |
;; and there should be derived CLIMATE-ON-OFF-CONTROLLER class | |
(define-cell-class on-off-controller (cell-fsm) | |
() | |
(:io | |
(mode "Mode" (:type '((0 :off) (1 :heat)))) | |
(switch "Switch" (:type 'boolean))) | |
(:input | |
(target "Target value") | |
(sensor "Sensor")) | |
(:cells | |
(diff "Diff" (:formula (- .sensor .target))) | |
(tolerance "Tolerance" (:value 0.1d0))) | |
#++ | |
(:tag-rules hass-rules)) | |
(defmethod initial-state ((onoff on-off-controller)) :off) | |
(define-state/cells ((onoff on-off-controller) :off) | |
(:enter | |
(setf .switch nil)) | |
(:transitions | |
((and (eq :heat .mode) | |
(> .sensor (+ .target .tolerance))) | |
:heat) | |
((eq :heat .mode) :idle))) | |
(define-state/cells ((onoff on-off-controller) :idle) | |
(:enter | |
(setf .switch nil)) | |
(:transitions | |
((equal :off .mode) :off) | |
((< .sensor (- .target .tolerance)) :heat) | |
(.switch (setf .switch nil) :idle))) | |
(define-state/cells ((onoff on-off-controller) :heat) | |
(:enter | |
(setf .switch t)) | |
(:transitions | |
((equal :off .mode) :off) | |
((> .sensor (+ .target .tolerance)) :idle) | |
((not .switch) (setf .switch t) :heat))) | |
(define-cell-class timed-condition (cell-fsm) | |
((period :accessor period-of | |
:initform (error "must specify :PERIOD") | |
:initarg :period) | |
(active-value :accessor active-value-of | |
:initform t | |
:initarg :active-value) | |
(one-way-p :accessor one-way-p | |
:initform nil | |
:initarg :one-way-p) | |
(reactivate-delay :accessor reactivate-delay-of | |
:initform nil | |
:initarg :reactivate-delay)) | |
(:input | |
(condition "Condition" (:type 'boolean))) | |
(:io | |
(output "Output" (:type 'boolean)))) | |
(defmethod initial-state ((tcond timed-condition)) :decide) | |
(define-cell-method .activated ((tcond timed-condition)) | |
(equal .output (active-value-of tcond))) | |
(define-state/cells ((tcond timed-condition) :decide (cell-wait-state)) | |
(:transitions | |
((and (one-way-p tcond) .condition (.activated)) :active) | |
(.condition :wait) | |
((not .condition) :inactive))) | |
(define-state/cells ((tcond timed-condition) :inactive (cell-wait-state)) | |
(:enter | |
(unless (one-way-p tcond) | |
(setf .output (not (active-value-of tcond))))) | |
(:transitions | |
((and (one-way-p tcond) .condition (.activated)) | |
:active) | |
(.condition :wait) | |
((and (not (one-way-p tcond)) (.activated)) | |
:inactive))) | |
(define-state/cells ((tcond timed-condition) :wait (cell-wait-state)) | |
(:enter | |
(unless (one-way-p tcond) | |
(setf .output (not (active-value-of tcond)))) | |
(fsm-timeout tcond (period-of tcond))) | |
(:transitions | |
((not .condition) :inactive) | |
(:timeout :active))) | |
(define-state/cells ((tcond timed-condition) :active (cell-wait-state)) | |
(:enter | |
(setf .output (active-value-of tcond))) | |
(:transitions | |
((not .condition) :inactive) | |
((and (one-way-p tcond) (not (.activated)) (not (reactivate-delay-of tcond))) :wait) | |
((and (one-way-p tcond) (not (.activated)) (reactivate-delay-of tcond)) :delay) | |
((not (.activated)) :active))) | |
(define-state/cells ((tcond timed-condition) :delay (cell-wait-state)) | |
(:enter | |
(fsm-timeout tcond (or (reactivate-delay-of tcond) 0))) | |
(:transitions | |
(:timeout :wait))) | |
(define-cell-class cell-adjuster (cell-fsm) | |
((delay :accessor delay-of | |
:initarg :delay | |
:initform 0.1d0)) | |
(:input | |
(enabled "Adjuster enabled" (:type 'boolean)) | |
(input "Input cell")) | |
(:io | |
(output "Output cell"))) | |
(defmethod initial-state ((adjuster cell-adjuster)) :inactive) | |
(define-state/cells ((adjuster cell-adjuster) :inactive (cell-wait-state)) | |
(:transitions | |
(.enabled :active))) | |
(define-state/cells ((adjuster cell-adjuster) :active (cell-wait-state)) | |
(:transitions | |
((not .enabled) :inactive) | |
((not (equal .input .output)) | |
;; TBD: better rate limiting | |
(setf .output .input) | |
:pause))) | |
(define-state/cells ((adjuster cell-adjuster) :pause (cell-wait-state)) | |
(:enter | |
(fsm-timeout adjuster (delay-of adjuster))) | |
(:transitions | |
((not .enabled) :inactive) | |
(:timeout :active))) | |
(define-cell-class mqtt-action-handler (cell-context) | |
((action-mappings :accessor action-mappings-of :initform (make-hash-table :test #'equal))) | |
(:cells | |
;; TBD: implement action queue | |
;; and feedback via the .ACTION-HANDLED cell | |
(pending-action (:type 'symbol)))) | |
(defmethod initialize-instance :after ((handler mqtt-action-handler) | |
&key action-mappings parent | |
(pmc (object-by-class parent 'persistent-mqtt-client)) | |
&allow-other-keys) | |
;; TODO: auto-unobserve upon unparenting (removal) | |
(observe-pmc-message-received pmc handler) | |
(iter (for (topic payload action) in action-mappings) | |
(setf (gethash (cons topic payload) (action-mappings-of handler)) action) | |
(pmc-subscribe pmc topic))) | |
(defmethod pmc-message-received ((pmc persistent-mqtt-client) (handler mqtt-action-handler) | |
topic payload retain) | |
(when-let ((action (gethash (cons topic payload) (action-mappings-of handler)))) | |
(cond (retain | |
(warn "Not running action ~s for _retained_ message ~s on topic ~s" | |
action payload topic)) | |
(t | |
(dbg "MQTT-ACTION-HANDLER: topic ~s payload ~s -> action ~s" | |
topic payload action) | |
(setf (cell-value 'pending-action handler) action))))) | |
(define-cell-class izba-commander (cell-fsm) | |
() | |
(:io (.pending-action (:type 'symbol))) | |
(:bind :pending-action :mqtt-action-handler.pending-action)) | |
(defmethod initial-state ((commander izba-commander)) :initial) | |
(define-state/cells ((commander izba-commander) :initial (cell-wait-state)) | |
(:transitions | |
((eq :outdoor-lights-on .pending-action) | |
;; only turn on the outdoor lights on the house and in front of it | |
;; but not at the stable | |
(setf .house-outdoor-lights.is-on t | |
.pending-action nil) | |
:initial) | |
((eq :outdoor-lights-off .pending-action) | |
;; Turn off all of the lights, including those at the stable | |
;; (later should also turn on the side floodlight on the house) | |
(setf .outdoor-lights.is-on nil | |
.pending-action nil | |
.izba.lights-turned-off t) | |
:initial) | |
((eq :away-mode-on .pending-action) | |
(mqtt-notify "Away Mode On" "Away Mode Turned On") | |
(setf .izba.away t | |
.pending-action nil) | |
:initial) | |
((eq :away-mode-off .pending-action) | |
(mqtt-notify "Away Mode Off" "Away Mode Turned Off") | |
(setf .izba.away nil | |
.pending-action nil) | |
:initial) | |
((and (not .izba.dark) .izba.lights-turned-off) | |
(setf .izba.lights-turned-off nil) | |
:initial))) | |
#++ | |
(define-cell-class rmme (cell-context) | |
() | |
(:cells | |
(a) | |
(b) | |
(enabled (:type 'boolean)) | |
(condition (:type 'boolean)) | |
(cond-output (:type 'boolean)))) | |
#++ | |
(reg 'rmme) | |
#++ | |
(reg 'timed-condition | |
:name 'rmme-condition | |
:period 10d0 | |
:bind '(:condition :rmme.condition | |
:output :rmme.cond-output)) | |
#++ | |
(reg 'cell-adjuster | |
:name 'rmme-adjuster | |
:bind '(:input :rmme.a | |
:output :rmme.b | |
:enabled :rmme.enabled)) | |
(define-hass-local-device room-temps (hass-local-device) | |
() | |
(:desc "Room temperatures") | |
(:node-id "eacs-room-temps") | |
(:cells | |
(.lobby-climate-mode "Lobby climate mode" :climate-mode (:value :heat)) | |
(.lobby-climate-temp "Lobby" (:value 20d0)) | |
(.dining-room-climate-mode "Dining room climate mode" :climate-mode (:value :heat)) | |
(.dining-room-climate-temp "Dining room" (:value 20d0)) | |
(.hall1-climate-mode "Hall 1 climate mode" :climate-mode (:value :heat)) | |
(.hall1-climate-temp "Hall 1" (:value 20d0)) | |
(.bedroom1-climate-mode "Bedroom 1 climate mode" :climate-mode (:value :heat)) | |
(.bedroom1-climate-temp "Bedroom 1" (:value 20d0)) | |
(.bedroom2-climate-mode "Bedroom 2 climate mode" :climate-mode (:value :heat)) | |
(.bedroom2-climate-temp "Bedroom 2" (:value 20d0)) | |
(.mishas-room-climate-mode "Misha's room climate mode" :climate-mode (:value :heat)) | |
(.mishas-room-climate-temp "Misha's room" (:value 20d0))) | |
(:bind | |
;; These cells are added as :IO fallback cells | |
;; by the DEFINE-HASS-LOCAL-DEVICE macro as they're present | |
;; in :OBJECTS but are not mentioned in any of cell sections: | |
;; :CELLS :GLOBAL-CELLS :INPUT :OUTPUT :IO | |
:lobby-actual-temp :hass.octopus-1stfloor.lobby | |
:dining-room-actual-temp :hass.octopus-1stfloor.dining-room-temp | |
:hall1-actual-temp :hass.octopus-1stfloor.hall | |
:bedroom1-actual-temp :hass.octopus-1stfloor.bedroom | |
:bedroom2-actual-temp :hass.freshair.2nd-floor-bedroom-temperature | |
:mishas-room-actual-temp :hass.light-floor-2-1.temperature-in-mishas-room) | |
(:objects | |
(:climate "lobby" | |
:mode-cell .lobby-climate-mode | |
:set-temperature-cell .lobby-climate-temp | |
:current-temperature-cell .lobby-actual-temp) | |
(:climate "dining-room" | |
:mode-cell .dining-room-climate-mode | |
:set-temperature-cell .dining-room-climate-temp | |
:current-temperature-cell .dining-room-actual-temp) | |
(:climate "hall-1" | |
:mode-cell .hall1-climate-mode | |
:set-temperature-cell .hall1-climate-temp | |
:current-temperature-cell .hall1-actual-temp) | |
(:climate "bedroom-1" | |
:mode-cell .bedroom1-climate-mode | |
:set-temperature-cell .bedroom1-climate-temp | |
:current-temperature-cell .bedroom1-actual-temp) | |
(:climate "bedoom-2" | |
:mode-cell .bedroom2-climate-mode | |
:set-temperature-cell .bedroom2-climate-temp | |
:current-temperature-cell .bedroom2-actual-temp | |
:precision 0.1d0) | |
(:climate "mishas-room" | |
:mode-cell .mishas-room-climate-mode | |
:set-temperature-cell .mishas-room-climate-temp | |
:current-temperature-cell .mishas-room-actual-temp)) | |
#++ | |
(:tag-rules hass-rules)) | |
(define-cell-class izba (cell-context) | |
() | |
(:cells | |
;; ZZZZZ !!! fixme | |
(night (:type 'boolean) (:formula (time-between .real-time '(19 30) #++ '(23 00) '(10 00)))) | |
(dark (:type 'boolean) | |
#++(:formula | |
;; FIXME: UTC | |
(time-between .real-time '(15 00) #++ '(23 00) '(5 00)) | |
#++(time-between .real-time '(18 00) #++ '(23 00) '(8 00))) | |
(:formula (< .hass.weather.illumination 100d0))) | |
(away (:type 'boolean)) | |
(lights-turned-off (:type 'boolean)) | |
(need-outdoor-lights | |
(:type 'boolean) | |
(:formula (and .dark (not .lights-turned-off)))) | |
(auto-temps (:type 'boolean) (:value t)) | |
(bedroom-2-night-temp "Bedroom 2 night temperature" (:value 18d0)) | |
(low-room-temp "Low room temperature" (:value 18d0)) | |
(normal-room-temp "Normal room temperature" (:value 22d0)) | |
(lobby-target-temp "Lobby temp" (:value 15d0)) | |
(away-temp "Away temp" (:value 20d0)) ;; FIXME: 15d0 | |
(dining-room-target-temp | |
(:value 18d0) | |
#++ | |
(:formula (cond (.away .away-temp) | |
(.night .low-room-temp) | |
(t (+ .normal-room-temp 1d0))))) | |
(hall-1-target-temp | |
;; FIXME | |
(:value -1d0) | |
#++ | |
(:formula | |
;; unfortunate sensor placement | |
(cond (.away .away-temp) | |
(.night .low-room-temp) | |
(t (+ .normal-room-temp 1.5d0))))) | |
(bedroom-1-target-temp | |
(:formula 23.5d0) | |
#++ | |
(:formula -1d0 #++(cond (.away .away-temp) | |
(.night .normal-room-temp) | |
(t .low-room-temp)))) | |
(bedroom-2-target-temp | |
(:value 15d0) | |
#++ | |
(:formula (cond (.away .away-temp) | |
(.night .bedroom-2-night-temp) | |
(t .low-room-temp)))) | |
(mishas-room-target-temp | |
(:value 15d0) | |
;; FIXME: weekends | |
#++ | |
(:formula (cond (.away .away-temp) | |
(.night .low-room-temp) | |
(t 18d0 #++ .normal-room-temp)))) | |
(min-temp-diff | |
(:formula | |
(min .bedroom1-temp-control.diff | |
.dining-room-temp-control.diff | |
.hall1-temp-control.diff | |
.lobby-temp-control.diff | |
;; FIXME: inconsistent naming | |
.bedroom-2-temp-control.diff | |
.mishas-room-temp-control.diff))) | |
(radiator-temp | |
(:formula | |
;; TBD: use average temperature over some period | |
;; or at least use longer interval for the cell-adjuster | |
;; See MAVP "moving average with variable period" | |
(if (not .hass.boiler.radiator-circulation-relay) | |
10d0 | |
(let ((temp | |
(cond ((not .hass.boiler.radiator-circulation-relay) 10d0) | |
;; make it warmer when possible | |
((and .hass.boiler.boiler-circulation-relay | |
(> .hass.boiler.temp-burner 200d0)) | |
70d0) | |
;; not using a simple linear formula for the | |
;; sake of easier adjustability | |
((> .hass.weather.temperature 25d0 ) 10d0) | |
((> .hass.weather.temperature 10d0 ) 30d0) | |
((> .hass.weather.temperature 5d0 ) 35d0) | |
((> .hass.weather.temperature 0d0 ) 40d0) | |
((> .hass.weather.temperature -5d0 ) 45d0) | |
((> .hass.weather.temperature -10d0) 50d0) | |
((> .hass.weather.temperature -15d0) 55d0) | |
((> .hass.weather.temperature -20d0) 60d0) | |
((> .hass.weather.temperature -25d0) 65d0) | |
(t 70d0)))) | |
(min 70d0 | |
(cond ((<= .izba.min-temp-diff -5d0) 70d0) | |
((<= .izba.min-temp-diff -2d0) (+ temp 20d0)) | |
((<= .izba.min-temp-diff -1d0) (+ temp 10d0)) | |
((<= .izba.min-temp-diff -0.5d0) (+ temp 5d0)) | |
(t temp)))))))) | |
;; Planned child specs. | |
;; Child objects can reference cells relative to this context. | |
;; Also, there can be cell declarations among bindings. | |
#++ | |
(:children | |
(relaxed-multi-switch | |
(:name 'outdoor-lights | |
:cells '(:hass.light-floor-2-2.front-floodlight | |
:hass.light-bbq.bbq-light | |
:hass.light-shack.shack-light | |
:hass.light-floor-1-2.porch-light | |
:hass.stablelight.stable-outdoor-light))) | |
(timed-condition | |
(:name 'outdoor-lights-cond | |
:one-way-p t :period 600 | |
:reactivate-delay (* 3600 12)) | |
(:bind | |
;; Could also be just (:condition .dark) | |
;; to refer to a previously defined cell. | |
;; The new cell can only be declared if it's not | |
;; declared elsewhere, so with below line, "dark" | |
;; above would have to be removed | |
(:condition dark (:type 'boolean) (:formula (< .hass.weather.illumination 100d0))) | |
;; Note that this cell reference can be resolved relative | |
;; to this context. That feature should be added to EXPRESSO | |
(:output .outdoor-lights.is-on))))) | |
(define-hass-local-device livestock-temps (hass-local-device) | |
() | |
(:desc "Livestock building temperatures") | |
(:node-id "livestock-temps") | |
(:cells | |
(.stable-climate-mode "Stable climate mode" :climate-mode (:value :heat)) | |
(.stable-climate-temp "Stable" (:value 8d0))) | |
(:bind | |
:stable-actual-temp :hass.stable-climate.stable-climate-temperature) | |
(:objects | |
(:climate "stable" | |
:mode-cell .stable-climate-mode | |
:set-temperature-cell .stable-climate-temp | |
:current-temperature-cell .stable-actual-temp)) | |
#++ | |
(:tag-rules hass-rules)) | |
(define-cell-class stable-co2 (cell-fsm) | |
() | |
(:io | |
(switch "Switch" (:type 'boolean))) | |
(:input | |
(sensor "Sensor")) | |
(:cells | |
(enabled "Enabled" (:type 'boolean) (:value t)) | |
(low-co2-value (:value 1100d0) "Low CO2 value") | |
(high-co2-value (:value 1500d0) "High CO2 value")) | |
(:bind | |
:sensor :hass.stable-climate.stable-climate-co2 | |
:switch :hass.stablelight.stable-fans) | |
#++ | |
(:tag-rules hass-rules)) | |
(defmethod initial-state ((ctl stable-co2)) :disabled) | |
(define-state/cells ((ctl stable-co2) :disabled) | |
(:transitions | |
((and .enabled (< .sensor .high-co2-value)) :off) | |
(.enabled :on))) | |
(define-state/cells ((ctl stable-co2) :off (cell-wait-state)) | |
(:enter | |
(setf .switch nil)) | |
(:transitions | |
((not .enabled) :disabled) | |
(.switch :off) | |
((>= .sensor .high-co2-value) :on))) | |
(define-state/cells ((ctl stable-co2) :on (cell-wait-state)) | |
(:enter | |
(setf .switch t)) | |
(:transitions | |
((not .enabled) :disabled) | |
((not .switch) :on) | |
((< .sensor .low-co2-value) :off))) | |
#++ | |
(define-cell-class stable-co2 (on-off-controller) | |
() | |
(:cells | |
(mode "Mode" (:type '((0 :off) (1 :heat))) (:value :heat)) ;; FIXME | |
(tolerance (:value 200d0)) | |
(target (:value 1100d0))) | |
(:bind | |
:sensor :hass.stable-climate.stable-climate-co2 | |
:switch :hass.stablelight.stable-fans)) | |
(defun reg-adjuster (enabled input output &optional (delay nil delay-p)) | |
(apply #'reg 'cell-adjuster | |
:name (symbolicate output 'adjuster) | |
:bind (list :enabled enabled | |
:input input | |
:output output) | |
(when delay-p (list :delay delay)))) | |
(defun reg-adjusters (enabled &rest pairs) | |
(doplist (input output pairs) | |
(reg-adjuster enabled input output))) | |
(defparameter *action-mappings* | |
'(("light_floor_2_1/home_office_button_action" "doubleclick" :away-mode-off ) | |
("light_floor_2_1/home_office_button_action" "longclick" :away-mode-on ) | |
("light_floor_1_1/dining_room_1_button_action" "doubleclick" :outdoor-lights-on ) | |
("light_floor_1_1/dining_room_1_button_action" "longclick" :outdoor-lights-off))) | |
(reg 'izba) | |
(reg 'izba-commander) | |
(reg 'livestock-temps :id "ba58e7ab-c647-44c1-b62c-01e28a2eecdd") | |
(reg 'stable-co2) | |
(reg 'mqtt-action-handler :action-mappings *action-mappings*) | |
;; (reg 'local-dev-test :id "584b13ba-6057-4b26-92c7-936c13a535c3") | |
;; (reg 'local-dev-test-2 :id "d3b0f8b2-93df-4ee8-a211-4ab92a39de3c") | |
#++ | |
(reg 'multi-switch | |
:name 'tst-light | |
:cells '(:hass.light-floor-2-1.home-office-light | |
:hass.light-floor-2-2.hall-2nd-floor-light)) | |
(reg 'relaxed-multi-switch | |
:name 'outdoor-lights | |
:cells '(:hass.light-floor-2-2.front-floodlight | |
#++ :hass.light-bbq.bbq-light | |
:hass.light-shack.shack-light | |
:hass.light-floor-1-2.porch-light | |
:hass.stablelight.stable-outdoor-light)) | |
(reg 'relaxed-multi-switch | |
:name 'house-outdoor-lights | |
:cells '(:hass.light-floor-2-2.front-floodlight | |
#++ :hass.light-bbq.bbq-light | |
:hass.light-shack.shack-light | |
:hass.light-floor-1-2.porch-light)) | |
(reg 'timed-condition | |
:name 'outdoor-lights-cond | |
:one-way-p t | |
:period 600 | |
;; Only reactivate during the next day | |
;; FIXME: this is not very good in case of an accidental activation | |
;; :reactivate-delay (* 3600 12) | |
:bind '(.condition .izba.need-outdoor-lights | |
.output .outdoor-lights.is-on)) | |
(reg 'boiler :parent-class 'mqtt-hass-discovery-client) | |
(reg 'multi-switch | |
:name 'dining-room-radiators | |
:cells '(:hass.boiler.radiator-1-dining-room-south-west | |
:hass.boiler.radiator-1-dining-room-north-west | |
:hass.boiler.radiator-1-kitchen)) | |
(reg 'multi-switch | |
:name 'bedroom-1-radiators | |
:cells '(:hass.boiler.radiator-1-bedroom-north | |
:hass.boiler.radiator-1-bedroom-east)) | |
(reg 'multi-switch | |
:name 'mishas-room-radiators | |
:cells '(:hass.boiler.radiator-2-mishas-room-south | |
:hass.boiler.radiator-2-mishas-room-west)) | |
(reg 'multi-switch | |
:name 'bedroom-2-radiators | |
:cells '(:hass.boiler.radiator-2-bedroom-west | |
:hass.boiler.radiator-2-bedroom-north)) | |
(reg 'timed-condition | |
:name 'no-radiator-circulation-cond | |
:period 1800d0 | |
:active-value nil | |
;; TBD: should actually turn them off only when | |
;; bathroom temperature is above the specified threshold | |
:bind '(.condition .hass.boiler.circulation-not-needed-p | |
.output .hass.boiler.radiator-circulation-relay)) | |
(reg 'timed-on-off | |
:name 'timed-on-off | |
:time-a '(8 0) | |
:time-b '(22 0) | |
:back-to-off-delay 1500d0 | |
:bind '(:target-cell :hass.coop4ch.coop-light)) | |
(reg 'room-temps :id "5727070d-4ac4-4ab0-aed6-f7deee843713") | |
(reg 'on-off-controller | |
:name 'lobby-temp-control | |
;; TBD: rm .izba-umbrella prefix!!!! | |
:bind '(:mode :room-temps.lobby-climate-mode | |
:target :room-temps.lobby-climate-temp | |
:sensor :hass.octopus-1stfloor.lobby | |
:switch :hass.boiler.radiator-1-lobby)) | |
(reg 'on-off-controller | |
:name 'dining-room-temp-control | |
:bind '(:mode :room-temps.dining-room-climate-mode | |
:target :room-temps.dining-room-climate-temp | |
:sensor :hass.octopus-1stfloor.dining-room-temp | |
:switch :dining-room-radiators.is-on)) | |
(reg 'on-off-controller | |
:name 'bedroom1-temp-control | |
:bind '(:mode :room-temps.bedroom1-climate-mode | |
:target :room-temps.bedroom1-climate-temp | |
:sensor :hass.octopus-1stfloor.bedroom | |
:switch :bedroom-1-radiators.is-on)) | |
(reg 'on-off-controller | |
:name 'hall1-temp-control | |
:bind '(:mode :room-temps.hall1-climate-mode | |
:target :room-temps.hall1-climate-temp | |
:sensor :hass.octopus-1stfloor.hall | |
:switch :hass.boiler.radiator-1-hall)) | |
(reg 'on-off-controller | |
:name 'mishas-room-temp-control | |
:bind '(:mode :room-temps.mishas-room-climate-mode | |
:target :room-temps.mishas-room-climate-temp | |
:sensor :hass.light-floor-2-1.temperature-in-mishas-room | |
:switch :mishas-room-radiators.is-on)) | |
(reg 'on-off-controller | |
:name 'bedroom-2-temp-control | |
:bind '(:mode :room-temps.bedroom2-climate-mode | |
:target :room-temps.bedroom2-climate-temp | |
:sensor :hass.freshair.2nd-floor-bedroom-temperature | |
:switch :bedroom-2-radiators.is-on)) | |
(reg 'on-off-controller | |
;; TBD: more tolerance | |
:name 'stable-temp-control | |
:bind '(:mode :livestock-temps.stable-climate-mode | |
:target :livestock-temps.stable-climate-temp | |
:sensor :hass.stable-climate.stable-climate-temperature | |
:switch :hass.socket1.smart-socket-1)) | |
(reg-adjusters | |
:izba.auto-temps | |
:izba.dining-room-target-temp :room-temps.dining-room-climate-temp | |
:izba.hall-1-target-temp :room-temps.hall1-climate-temp | |
:izba.bedroom-1-target-temp :room-temps.bedroom1-climate-temp | |
:izba.bedroom-2-target-temp :room-temps.bedroom2-climate-temp | |
:izba.mishas-room-target-temp :room-temps.mishas-room-climate-temp | |
:izba.lobby-target-temp :room-temps.lobby-climate-temp | |
:izba.radiator-temp :hass.boiler.target-temp | |
;; FIXME: shouldn't be disabled with auto-temps | |
:hass.light-floor-1-1.dining-room-light-1 :hass.light-floor-1-2.dining-room-light-2) | |
;; the following is not good due to circulation control | |
;; (reg-adjuster :izba.auto-temps :izba.radiator-temp :hass.boiler.target-temp 1800d0) | |
(defun izba-startup (event-loop &optional (configs '())) | |
(apply #'load-config (alexandria:ensure-list configs)) | |
(let* ((cs-view (make-instance 'cs-view :name 'izba-view :parent event-loop)) | |
(umbrella (make-instance 'timed-umbrella | |
:name 'izba-umbrella | |
:parent cs-view | |
:model (cell-model-of cs-view)))) | |
;; FIXME!!!!! umbrella setup should work w/o IN-CS-VIEW!!! | |
(in-cs-view cs-view) | |
(setf *pmc* (make-instance 'persistent-mqtt-client | |
:parent cs-view | |
:client-id "izba2" | |
:host *mqtt-server* | |
:port 1883) | |
*ds* (make-instance 'mqtt-data-source | |
:parent cs-view | |
:pmc *pmc*) | |
*mhdc* (make-instance 'mqtt-hass-discovery-client | |
:name 'hass | |
:pmc *pmc* | |
:parent cs-view | |
:model (cell-model-of cs-view) | |
;; TBD: this shouldn't be needed! | |
:ignore-devices '("local-dev-test" | |
"local-dev-test-2" | |
"eacs-room-temps" | |
"livestock-temps")) | |
;; *boiler* (make-instance 'boiler | |
;; :name 'boiler | |
;; :parent *mhdc* | |
;; :model (cell-model-of cs-view)) | |
) | |
(setf (data-sink-model (make-instance 'mqtt-data-sink | |
:parent cs-view | |
:pmc *pmc*)) | |
(cell-model-of cs-view)) | |
(dolist (name (sort (hash-table-keys *to-create*) #'string<)) | |
(destructuring-bind (class-name &rest initargs) | |
(gethash name *to-create*) | |
(apply #'make-instance | |
class-name | |
:parent (get-parent-from-initargs umbrella initargs) | |
:model (cell-model-of cs-view) | |
:name (getf initargs :name class-name) | |
(remove-from-plist initargs :name :parent-name :parent-class)))) | |
#++ | |
(pmc-subscribe *pmc* "/devices/milur305_255/controls/Phase A reactive power") | |
(cs-view-add-data-source cs-view *ds*) | |
#++ | |
(make-instance 'roomtemp-control | |
:name 'roomtemp-control | |
:model *current-cell-model* | |
:parent cs-view) | |
(setf (active-p *pmc*) t) | |
#++ | |
(untrace-cells 'room-temp 'water-temp) | |
#++ | |
(trace-cells 'room-temp 'water-temp 'sum 'prev-room-temp 'target-room-temp) | |
#++ | |
(fsm-trace 'roomtemp-control) | |
(fsm-trace 'timed-condition | |
'on-off-controller | |
'multi-switch | |
'relaxed-multi-switch | |
'timed-on-off) | |
(process-events *root*))) | |
(defun toplevel-izba (&optional conf-file) | |
(setf cs-common::*object-loop-guard* t | |
*fsm-trace-time* :real) | |
(as-repl:start-async-repl | |
#'(lambda () | |
;; FIXME | |
(as:with-delay (0.3) | |
(setf #++ swank::*slime-repl-eval-hooks* | |
(symbol-value (cl-async-repl::repl-hook-sym)) | |
(cons 'repl-hook | |
(remove 'repl-hook | |
(symbol-value (cl-async-repl::repl-hook-sym)) | |
#++ swank::*slime-repl-eval-hooks*)))) | |
(izba-startup | |
(setf *root* (get-async-event-loop)) | |
conf-file) | |
;; FIXME: rm !!! | |
#++ | |
(as:with-delay (10) | |
(all-visible nil))))) | |
;; FIXME | |
(defun repl-hook (form) | |
(declare (ignore form)) | |
(when (running-p) | |
(cl-async-repl::sync-eval | |
'(ignore-errors | |
(as:with-delay (0.1) | |
(when *root* | |
(process-events *root*)))))) | |
#++ (swank:repl-eval-hook-pass) | |
(funcall (find-symbol "REPL-EVAL-HOOK-PASS" :swank))) | |
(defun all-visible (&optional trace) | |
(let ((all-cells (all-cells))) | |
(dolist (c all-cells) | |
(setf (visible-via c t) t)) | |
(apply (if trace | |
#'trace-cells | |
#'untrace-cells) | |
all-cells))) | |
(defun print-online (&optional substring (model *current-cell-model*)) | |
(with-cell-model (model) | |
(format t "~:{~a~60t= ~a~%~}" | |
(iter (for c in (sort (all-cells) #'string<)) | |
(when (and (or (null substring) | |
(search substring (cell-title c) :test #'equalp) | |
(search substring (string c) :test #'equalp)) | |
(eq :online (cell-state c))) | |
(collect (list c | |
(let ((v (cell-value c))) | |
(if (floatp v) | |
(format nil "~,4f" v) | |
v))))))))) | |
#++ | |
(pushnew 'repl-hook swank:*slime-repl-eval-hooks*) | |
#++ | |
(toplevel-izba) | |
#++ | |
(dolist (c (all-cells)) (setf (visible-via c t) t)) | |
#++ | |
(apply #'trace-cells (all-cells)) | |
#++ | |
(avadakedavra) | |
#++ | |
(observe-cell :hass.boiler.mix-factor :watch | |
#'(lambda (c) | |
(declare (ignore c)) | |
(print-online ".boiler."))) | |
(defun mqtt-notify (title message) | |
(pmc-publish *pmc* "hass-notify/ivan4th" | |
(st-json:write-json-to-string | |
(st-json:jso "title" title "message" message)))) | |
;; valveY 15.04 mixFactor ~0.990 | |
;; valveY 20.00 mixFactor ~0.986 | |
;; valveY 30.00 mixFactor ~0.980 | |
;; valveY 40.00 mixFactor ~0.974 ?? 0.879?? | |
;; valveY 50.00 mixFactor ~0.923 (~0.938??) | |
;; valveY 60.00 mixFactor ~0.574 (?? too hot) | |
;; (- .temp-house-to-tank .temp-tank-to-house) = ~ 0.28 ~44 degC | |
;; (- .temp-house-to-tank .temp-tank-to-house) = ~ 0.33 ~33 degC | |
;; (- .temp-house-to-tank .temp-tank-to-house) = ~ 0.40 ~29 degC | |
;; (- .temp-tank-to-house .temp-tank-a) = ~ -1.06 -1.22 ~52.17 degC temp | |
;; make sure mqtt reconnect is working! | |
(defun intern-cells () | |
(dolist (c (all-cells)) | |
(intern (format nil ".~a" c)))) | |
(defun o (type) | |
(object-by-class *root* type)) | |
(defun oo (type) | |
(objects-by-class *root* type)) | |
(defun obn (name) | |
(object-by-name *root* name)) | |
(defun dump-object-tree (&optional (start-from *root*)) | |
(labels ((rec (object level) | |
(format t "~v@T~s: ~s~@[ (state: ~s)~]~%" | |
(* level 2) | |
(type-of object) | |
(name object) | |
(when (typep object 'state-machine) | |
(fsm-state-name object (fsm-state object)))) | |
(dolist (child (sort (copy-list (children object)) #'string< | |
:key #'(lambda (object) | |
(concat (type-of object) | |
" " | |
(name object))))) | |
(rec child (1+ level))))) | |
(rec start-from 0))) | |
(defvar *default-conf-file* #p"/etc/eacs.conf") | |
(defun save-server-image (path) | |
(save-image | |
path 4005 | |
#'(lambda () | |
(toplevel-izba | |
#++ ;; FIXME | |
(or (second #+ccl ccl:*command-line-argument-list* | |
#+sbcl sb-ext:*posix-argv* | |
#-(or sbcl ccl) nil) | |
*default-conf-file*))))) | |
;; TBD: !!!!! Rm ALL-VISIBLE call from TOPLEVEL-IZBA !!!!! | |
;; TBD: calculated cell deps not updated !!!! | |
;; .izba.radiator-temp ==> nil deps | |
;; (even though it does depend on a number of cells) | |
;; TBD: !!! stable climate: tolerance !!! | |
;; TBD: make it possible to specify initargs as cell values in DEFINE-CELL-CLASS | |
;; TBD: fix remote device naming (?) | |
;; Perhaps if the name has already hass.smth prefix, | |
;; don't add same hass. prefix to the cells | |
;; (but need to think about it) | |
;; TBD: in bind exprs, use dot notation instead of keywords | |
;; TBD: rm 2nd return value from (cx .smth) | |
;; also have multiple return values for each cell in (cx (dbg-show* .a .b .c)) | |
;; TBD: replacing mqtt-action-handler breaks (observation on PMC) | |
;; TBD: redefining boiler shouldn't break radiator multi-switches | |
;; TBD: the context should be considered complete if only unaliased | |
;; fallback cells are present. Incomplete = aliased fallback cells | |
;; missing or they don't match | |
;; TBD: precision somehow not working for climate | |
;; TBD: action_topic support for climate (off, heating, cooling, drying, idle, fan) | |
;; TBD: add proper avail topic to the boiler | |
;; TBD: make REG safe to call from the repl! | |
;; TBD: ON-OFF-CONTROLLER: detect abnormal values | |
;; (e.g. zero), go to off (updating .MODE) and send an alert | |
;; TBD: don't use global vars! | |
;; TBD: call process-events | |
;; TBD: will topic for the boiler | |
;; TBD: :ignore-devices shouldn't be needed for MHDC! | |
;; TBD: get rid of MQTT-CLIENT error that pops up after calling REG on TIMED-ON-OFF | |
;; TBD: add a mode for auto-internining cells | |
;; TBD: T as avail topic to mark the cell "always avail" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment