feat(sono): madrugada silenciosa e N sons para acordar

Antes um único som acima do limiar, um quadro de movimento ou um pulso
do PIR acordavam o rosto; deitado na cama, ele acordava toda hora.

- Som: precisa de `somAcordar` barulhos (padrão 3) em 1 minuto; o mesmo
  barulho comprido conta uma vez.
- Madrugada silenciosa (padrão 23h às 7h, configurável): movimento,
  rastreamento, PIR e modo presença não acordam; só barulho ALTO
  repetido, toque ou conversa.
- Vida: de madrugada não acorda sozinho e volta a dormir mesmo com gente
  se mexendo, a não ser que alguém tenha interagido nos últimos 10 min.
- Painel: grupo "Sono e madrugada" nos Ajustes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
julio
2026-10-06 04:48:11 -03:00
co-authored by Claude Opus 5.5
parent 5fcd038861
commit dd40c730be
8 changed files with 135 additions and 22 deletions
+32 -3
View File
@@ -741,6 +741,27 @@ const MIC = {
cooldown: parseFloat(par.get("somcooldown") || "5"), // s entre sustos cooldown: parseFloat(par.get("somcooldown") || "5"), // s entre sustos
}; };
/* Acordar com som: precisa de N barulhos em 1 minuto (ajuste "Sons para acordar"). Na madrugada
silenciosa só barulho ALTO conta, e movimento/câmera não acordam (alguém se mexendo na cama). */
const DESPERTAR = { sons: 3, janela: 60, noite: true, ini: 23, fim: 7, eventos: [], ultimo: -Infinity, estava: false };
function madrugada() {
if (!DESPERTAR.noite || DESPERTAR.ini === DESPERTAR.fim) return false;
const h = new Date().getHours();
return DESPERTAR.ini < DESPERTAR.fim ? h >= DESPERTAR.ini && h < DESPERTAR.fim : h >= DESPERTAR.ini || h < DESPERTAR.fim;
}
/* Conta o INÍCIO de cada barulho (o mesmo barulho comprido vale um) e diz se já dá para acordar. */
function barulhoAcorda(ouviu, agora) {
const D = DESPERTAR;
if (ouviu && !D.estava && agora - D.ultimo >= 1.5) {
D.ultimo = agora;
D.eventos = D.eventos.filter((t) => agora - t < D.janela).concat(agora);
console.log("ouvido: barulho " + D.eventos.length + "/" + D.sons + (madrugada() ? " (madrugada)" : ""));
}
D.estava = ouviu;
if (D.eventos.length >= D.sons && agora - D.eventos[D.eventos.length - 1] < 1) { D.eventos = []; return true; }
return false;
}
function som(nivel) { function som(nivel) {
const n = Math.max(0, Math.min(1, Number(nivel) || 0)); const n = Math.max(0, Math.min(1, Number(nivel) || 0));
const agora = performance.now() / 1000; const agora = performance.now() / 1000;
@@ -765,12 +786,14 @@ function som(nivel) {
console.log("ouvido nivel=" + n.toFixed(2) + " piso=" + (MIC.piso || 0).toFixed(2)); console.log("ouvido nivel=" + n.toFixed(2) + " piso=" + (MIC.piso || 0).toFixed(2));
} }
if (r.dormindo && temSom) { // acordou: tinha barulho if (r.dormindo) {
if (barulhoAcorda(madrugada() ? alto : temSom, agora)) { // acordou: barulho repetido
r.dormindo = false; r.dormindo = false;
animar("surpreso"); animar("surpreso");
MIC.proxSurpresa = agora + MIC.cooldown; MIC.proxSurpresa = agora + MIC.cooldown;
console.log("ouvido: acordou nivel=" + n.toFixed(2)); console.log("ouvido: acordou nivel=" + n.toFixed(2));
relatarSom("acordou", n); relatarSom("acordou", n);
}
} else if (alto && !MIC.estavaAlto && agora >= MIC.proxSurpresa) { } else if (alto && !MIC.estavaAlto && agora >= MIC.proxSurpresa) {
MIC.proxSurpresa = agora + MIC.cooldown; // susto: barulho alto e repentino MIC.proxSurpresa = agora + MIC.cooldown; // susto: barulho alto e repentino
animar("surpreso"); animar("surpreso");
@@ -857,7 +880,7 @@ function rastrear(t) {
MOV.olharY+=((t.y-.5)*2*MOV.olharMax*.6-MOV.olharY)*.65; MOV.olharY+=((t.y-.5)*2*MOV.olharMax*.6-MOV.olharY)*.65;
if(t.atividade===true) { if(t.atividade===true) {
MOV.ultimoMov=agora; MOV.ultimoMov=agora;
if(r.dormindo) { r.dormindo=false;animar('surpreso'); } if(r.dormindo && !madrugada()) { r.dormindo=false;animar('surpreso'); }
} }
} }
function movimento(vel, cx, cy) { function movimento(vel, cx, cy) {
@@ -887,12 +910,14 @@ function movimento(vel, cx, cy) {
} }
const forte = v >= MOV.limiarForte; const forte = v >= MOV.limiarForte;
if (r.dormindo && mexeu) { // acordou: algo se mexeu na frente if (r.dormindo) {
if (mexeu && !madrugada()) { // acordou: algo se mexeu na frente (de dia)
r.dormindo = false; r.dormindo = false;
animar("surpreso"); animar("surpreso");
MOV.proxSurpresa = agora + MOV.cooldown; MOV.proxSurpresa = agora + MOV.cooldown;
console.log("presenca: acordou nivel=" + v.toFixed(2)); console.log("presenca: acordou nivel=" + v.toFixed(2));
relatarMov("acordou", v); relatarMov("acordou", v);
}
} else if (forte && !MOV.estavaForte && agora >= MOV.proxSurpresa) { } else if (forte && !MOV.estavaForte && agora >= MOV.proxSurpresa) {
// susto so no INICIO do movimento forte: se continuar se mexendo, nao repete // susto so no INICIO do movimento forte: se continuar se mexendo, nao repete
MOV.proxSurpresa = agora + MOV.cooldown; MOV.proxSurpresa = agora + MOV.cooldown;
@@ -1040,6 +1065,10 @@ function configurarApp(c) {
EXTRAS.boca = c.boca !== false; EXTRAS.sobrancelhas = c.sobrancelhas === true; EXTRAS.iris = c.iris === true; EXTRAS.boca = c.boca !== false; EXTRAS.sobrancelhas = c.sobrancelhas === true; EXTRAS.iris = c.iris === true;
if (typeof c.subirBoca === 'number') EXTRAS.subirBoca = c.subirBoca; if (typeof c.subirBoca === 'number') EXTRAS.subirBoca = c.subirBoca;
SONO.apagar = c.apagarTela !== false; SONO.apagar = c.apagarTela !== false;
if (typeof c.somAcordar === 'number') DESPERTAR.sons = Math.max(1, Math.round(c.somAcordar));
DESPERTAR.noite = c.noiteSilenciosa !== false;
if (typeof c.noiteInicio === 'number') DESPERTAR.ini = Math.round(c.noiteInicio);
if (typeof c.noiteFim === 'number') DESPERTAR.fim = Math.round(c.noiteFim);
if (typeof c.apagarTelaMin === 'number') SONO.minutos = c.apagarTelaMin; if (typeof c.apagarTelaMin === 'number') SONO.minutos = c.apagarTelaMin;
if (typeof c.distSobrancelha === 'number') EXTRAS.distSobrancelha = c.distSobrancelha; if (typeof c.distSobrancelha === 'number') EXTRAS.distSobrancelha = c.distSobrancelha;
AUTO = c.auto; AUTO = c.auto;
+32 -3
View File
@@ -741,6 +741,27 @@ const MIC = {
cooldown: parseFloat(par.get("somcooldown") || "5"), // s entre sustos cooldown: parseFloat(par.get("somcooldown") || "5"), // s entre sustos
}; };
/* Acordar com som: precisa de N barulhos em 1 minuto (ajuste "Sons para acordar"). Na madrugada
silenciosa só barulho ALTO conta, e movimento/câmera não acordam (alguém se mexendo na cama). */
const DESPERTAR = { sons: 3, janela: 60, noite: true, ini: 23, fim: 7, eventos: [], ultimo: -Infinity, estava: false };
function madrugada() {
if (!DESPERTAR.noite || DESPERTAR.ini === DESPERTAR.fim) return false;
const h = new Date().getHours();
return DESPERTAR.ini < DESPERTAR.fim ? h >= DESPERTAR.ini && h < DESPERTAR.fim : h >= DESPERTAR.ini || h < DESPERTAR.fim;
}
/* Conta o INÍCIO de cada barulho (o mesmo barulho comprido vale um) e diz se já dá para acordar. */
function barulhoAcorda(ouviu, agora) {
const D = DESPERTAR;
if (ouviu && !D.estava && agora - D.ultimo >= 1.5) {
D.ultimo = agora;
D.eventos = D.eventos.filter((t) => agora - t < D.janela).concat(agora);
console.log("ouvido: barulho " + D.eventos.length + "/" + D.sons + (madrugada() ? " (madrugada)" : ""));
}
D.estava = ouviu;
if (D.eventos.length >= D.sons && agora - D.eventos[D.eventos.length - 1] < 1) { D.eventos = []; return true; }
return false;
}
function som(nivel) { function som(nivel) {
const n = Math.max(0, Math.min(1, Number(nivel) || 0)); const n = Math.max(0, Math.min(1, Number(nivel) || 0));
const agora = performance.now() / 1000; const agora = performance.now() / 1000;
@@ -765,12 +786,14 @@ function som(nivel) {
console.log("ouvido nivel=" + n.toFixed(2) + " piso=" + (MIC.piso || 0).toFixed(2)); console.log("ouvido nivel=" + n.toFixed(2) + " piso=" + (MIC.piso || 0).toFixed(2));
} }
if (r.dormindo && temSom) { // acordou: tinha barulho if (r.dormindo) {
if (barulhoAcorda(madrugada() ? alto : temSom, agora)) { // acordou: barulho repetido
r.dormindo = false; r.dormindo = false;
animar("surpreso"); animar("surpreso");
MIC.proxSurpresa = agora + MIC.cooldown; MIC.proxSurpresa = agora + MIC.cooldown;
console.log("ouvido: acordou nivel=" + n.toFixed(2)); console.log("ouvido: acordou nivel=" + n.toFixed(2));
relatarSom("acordou", n); relatarSom("acordou", n);
}
} else if (alto && !MIC.estavaAlto && agora >= MIC.proxSurpresa) { } else if (alto && !MIC.estavaAlto && agora >= MIC.proxSurpresa) {
MIC.proxSurpresa = agora + MIC.cooldown; // susto: barulho alto e repentino MIC.proxSurpresa = agora + MIC.cooldown; // susto: barulho alto e repentino
animar("surpreso"); animar("surpreso");
@@ -857,7 +880,7 @@ function rastrear(t) {
MOV.olharY+=((t.y-.5)*2*MOV.olharMax*.6-MOV.olharY)*.65; MOV.olharY+=((t.y-.5)*2*MOV.olharMax*.6-MOV.olharY)*.65;
if(t.atividade===true) { if(t.atividade===true) {
MOV.ultimoMov=agora; MOV.ultimoMov=agora;
if(r.dormindo) { r.dormindo=false;animar('surpreso'); } if(r.dormindo && !madrugada()) { r.dormindo=false;animar('surpreso'); }
} }
} }
function movimento(vel, cx, cy) { function movimento(vel, cx, cy) {
@@ -887,12 +910,14 @@ function movimento(vel, cx, cy) {
} }
const forte = v >= MOV.limiarForte; const forte = v >= MOV.limiarForte;
if (r.dormindo && mexeu) { // acordou: algo se mexeu na frente if (r.dormindo) {
if (mexeu && !madrugada()) { // acordou: algo se mexeu na frente (de dia)
r.dormindo = false; r.dormindo = false;
animar("surpreso"); animar("surpreso");
MOV.proxSurpresa = agora + MOV.cooldown; MOV.proxSurpresa = agora + MOV.cooldown;
console.log("presenca: acordou nivel=" + v.toFixed(2)); console.log("presenca: acordou nivel=" + v.toFixed(2));
relatarMov("acordou", v); relatarMov("acordou", v);
}
} else if (forte && !MOV.estavaForte && agora >= MOV.proxSurpresa) { } else if (forte && !MOV.estavaForte && agora >= MOV.proxSurpresa) {
// susto so no INICIO do movimento forte: se continuar se mexendo, nao repete // susto so no INICIO do movimento forte: se continuar se mexendo, nao repete
MOV.proxSurpresa = agora + MOV.cooldown; MOV.proxSurpresa = agora + MOV.cooldown;
@@ -1040,6 +1065,10 @@ function configurarApp(c) {
EXTRAS.boca = c.boca !== false; EXTRAS.sobrancelhas = c.sobrancelhas === true; EXTRAS.iris = c.iris === true; EXTRAS.boca = c.boca !== false; EXTRAS.sobrancelhas = c.sobrancelhas === true; EXTRAS.iris = c.iris === true;
if (typeof c.subirBoca === 'number') EXTRAS.subirBoca = c.subirBoca; if (typeof c.subirBoca === 'number') EXTRAS.subirBoca = c.subirBoca;
SONO.apagar = c.apagarTela !== false; SONO.apagar = c.apagarTela !== false;
if (typeof c.somAcordar === 'number') DESPERTAR.sons = Math.max(1, Math.round(c.somAcordar));
DESPERTAR.noite = c.noiteSilenciosa !== false;
if (typeof c.noiteInicio === 'number') DESPERTAR.ini = Math.round(c.noiteInicio);
if (typeof c.noiteFim === 'number') DESPERTAR.fim = Math.round(c.noiteFim);
if (typeof c.apagarTelaMin === 'number') SONO.minutos = c.apagarTelaMin; if (typeof c.apagarTelaMin === 'number') SONO.minutos = c.apagarTelaMin;
if (typeof c.distSobrancelha === 'number') EXTRAS.distSobrancelha = c.distSobrancelha; if (typeof c.distSobrancelha === 'number') EXTRAS.distSobrancelha = c.distSobrancelha;
AUTO = c.auto; AUTO = c.auto;
+14 -1
View File
@@ -22,6 +22,7 @@ PADRAO = dict(preview=False, auto=True, audio=True, volume=0.35,
economia=True, brilho=0.7, brilhoRepouso=0.08, repousoTela=30, economia=True, brilho=0.7, brilhoRepouso=0.08, repousoTela=30,
boca=True, sobrancelhas=False, iris=False, subirBoca=0, distSobrancelha=16, videoRTC=True, boca=True, sobrancelhas=False, iris=False, subirBoca=0, distSobrancelha=16, videoRTC=True,
apagarTela=True, apagarTelaMin=5, apagarTela=True, apagarTelaMin=5,
somAcordar=3, noiteSilenciosa=True, noiteInicio=23, noiteFim=7,
sensorMic=True, sensorCamera=True, micSono=True, cameraSono=False, sensorMic=True, sensorCamera=True, micSono=True, cameraSono=False,
cameraLado='user', videoResolucao='auto', videoFPS=0, cameraLado='user', videoResolucao='auto', videoFPS=0,
rastrearRosto=True, rastrearMaos=True, seguirRastreamento=True, mostrarRastreamento=True) rastrearRosto=True, rastrearMaos=True, seguirRastreamento=True, mostrarRastreamento=True)
@@ -32,7 +33,8 @@ LIMITES = dict(volume=(0, 1), somlim=(0, 1), somganho=(0, 1),
somaltolim=(0, 1), sompausa=(1, 3600), somcooldown=(0, 120), somaltolim=(0, 1), sompausa=(1, 3600), somcooldown=(0, 120),
movlim=(0, 1), movforte=(0, 1), movcooldown=(0, 120), movolhar=(0, 100), movlim=(0, 1), movforte=(0, 1), movcooldown=(0, 120), movolhar=(0, 100),
brilho=(0.01, 1), brilhoRepouso=(0.01, 1), repousoTela=(5, 3600), brilho=(0.01, 1), brilhoRepouso=(0.01, 1), repousoTela=(5, 3600),
subirBoca=(-20, 60), distSobrancelha=(0, 60), apagarTelaMin=(1, 120)) subirBoca=(-20, 60), distSobrancelha=(0, 60), apagarTelaMin=(1, 120),
somAcordar=(1, 20), noiteInicio=(0, 23), noiteFim=(0, 23))
def validar(dados): def validar(dados):
@@ -54,6 +56,17 @@ def validar(dados):
return dados return dados
def madrugada(config, agora=None):
"""Dentro do horário da madrugada silenciosa (ex.: 23h às 7h, atravessando a meia-noite)?"""
if not config.get('noiteSilenciosa', True):
return False
ini, fim = int(config.get('noiteInicio', 23)), int(config.get('noiteFim', 7))
hora = time.localtime(agora if agora is not None else time.time()).tm_hour
if ini == fim:
return False
return ini <= hora < fim if ini < fim else (hora >= ini or hora < fim)
def ip_informado(dados): def ip_informado(dados):
"""IP que o próprio app informa (aparelho.ip); o endereço da conexão pode ser de um proxy.""" """IP que o próprio app informa (aparelho.ip); o endereço da conexão pode ser de um proxy."""
aparelho = dados.get('aparelho') aparelho = dados.get('aparelho')
+6 -3
View File
@@ -32,7 +32,7 @@ from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import unquote from urllib.parse import unquote
from pathlib import Path from pathlib import Path
from comum import autorizado, carregar_token, gravar_atomico, ler_json from comum import autorizado, carregar_token, gravar_atomico, ler_json
from controle_pc import ControlePC, versao from controle_pc import ControlePC, madrugada, versao
from eventos import Eventos from eventos import Eventos
from servos import ControleServos from servos import ControleServos
from braco import Braco from braco import Braco
@@ -505,13 +505,15 @@ def main():
vida.celular_dormindo = celular_dormindo vida.celular_dormindo = celular_dormindo
vida.celular_online = lambda: servidor.controle.resumo()['online'] vida.celular_online = lambda: servidor.controle.resumo()['online']
vida.alguem_perto = alguem_perto vida.alguem_perto = alguem_perto
vida.madrugada = lambda agora: madrugada(servidor.controle.config, agora)
# E-Stop parado, ou modo presença com ninguém em casa (dormindo em silêncio): não faz nada # E-Stop parado, ou modo presença com ninguém em casa (dormindo em silêncio): não faz nada
vida.bloqueado = lambda: ESTOP["ativo"] or servidor.historico.presenca.get("estado") == "ausente" vida.bloqueado = lambda: ESTOP["ativo"] or servidor.historico.presenca.get("estado") == "ausente"
def presenca(tipo): def presenca(tipo):
vida.estimulo(tipo) vida.estimulo(tipo)
# PIR viu alguém: se o rosto estiver dormindo (e talvez com a tela apagada), acorda # PIR viu alguém: se o rosto estiver dormindo (e talvez com a tela apagada), acorda.
if tipo == "pir": # De madrugada não: alguém se mexendo na cama não é motivo para acordar.
if tipo == "pir" and not madrugada(servidor.controle.config):
with servidor.controle.lock: with servidor.controle.lock:
dormindo = bool(servidor.controle.estado.get("dormindo")) dormindo = bool(servidor.controle.estado.get("dormindo"))
if dormindo: if dormindo:
@@ -523,6 +525,7 @@ def main():
def alguem_chegou(): def alguem_chegou():
camera(True) # modo presença: o PIR liga a câmera do celular camera(True) # modo presença: o PIR liga a câmera do celular
if not madrugada(servidor.controle.config):
enfileirar("acordar") enfileirar("acordar")
def ninguem_por_perto(): def ninguem_por_perto():
+16 -4
View File
@@ -69,12 +69,24 @@ class VidaTest(unittest.TestCase):
self.v.necessidades['energia'] = 100 self.v.necessidades['energia'] = 100
self.assertEqual(self.v.agir(DIA + 120), 'acordar') self.assertEqual(self.v.agir(DIA + 120), 'acordar')
def test_noite_dorme_sem_ninguem(self): def test_madrugada_dorme_mesmo_com_gente_se_mexendo(self):
noite = time.mktime((2026, 10, 6, 23, 30, 0, 0, 0, -1)) noite = time.mktime((2026, 10, 6, 23, 30, 0, 0, 0, -1))
self.v.alguem_perto = lambda: True # deitado na cama, o PIR vê
self.assertEqual(self.v.agir(noite), 'dormir') self.assertEqual(self.v.agir(noite), 'dormir')
self.v.dormindo_proprio = False self.v.necessidades['energia'] = 100
self.v.alguem_perto = lambda: True self.assertIsNone(self.v.agir(noite + 60)) # descansado, mas é madrugada: não acorda
self.assertNotEqual(self.v.agir(noite), 'dormir') self.v.estimulo('carinho', noite + 120) # carinho acorda e segura acordado um tempo
self.assertFalse(self.v.dormindo())
self.assertNotEqual(self.v.agir(noite + 180), 'dormir')
self.assertEqual(self.v.agir(noite + 180 + 11 * 60), 'dormir')
def test_horario_da_madrugada(self):
from controle_pc import madrugada
h = lambda hora: time.mktime((2026, 10, 6, hora, 10, 0, 0, 0, -1))
cfg = dict(noiteSilenciosa=True, noiteInicio=23, noiteFim=7)
self.assertEqual([madrugada(cfg, h(x)) for x in (22, 23, 3, 6, 7, 12)], [False, True, True, True, False, False])
self.assertTrue(madrugada(dict(cfg, noiteInicio=1, noiteFim=5), h(2)))
self.assertFalse(madrugada(dict(cfg, noiteSilenciosa=False), h(3)))
def test_humor_e_gestos_de_carente_com_gente_perto(self): def test_humor_e_gestos_de_carente_com_gente_perto(self):
self.v.necessidades.update(energia=80, social=10) self.v.necessidades.update(energia=80, social=10)
+11 -5
View File
@@ -25,7 +25,8 @@ SONO_LENTO = 0.25
RITMOS = {'calmo': (60, 150), 'normal': (25, 75), 'agitado': (12, 35)} # segundos entre atos RITMOS = {'calmo': (60, 150), 'normal': (25, 75), 'agitado': (12, 35)} # segundos entre atos
PADRAO = dict(ligado=True, criar_emojis=True, max_emojis_dia=3, ritmo='normal', noite=True) PADRAO = dict(ligado=True, criar_emojis=True, max_emojis_dia=3, ritmo='normal', noite=True)
NOITE = (23, 7) # dorme das 23h às 7h, se ninguém estiver por perto NOITE = (23, 7) # horário padrão da madrugada (o servidor usa o dos ajustes do celular)
PEDIU_ATENCAO = 10 * 60 # de madrugada, só fica acordado se alguém interagiu (carinho, conversa) há pouco
INTERVALO_EMOJI = 90 * 60 # no mínimo 90 min entre duas figurinhas criadas sozinho INTERVALO_EMOJI = 90 * 60 # no mínimo 90 min entre duas figurinhas criadas sozinho
# O que cada estímulo faz com as necessidades (e o gesto que sai na hora, se houver). # O que cada estímulo faz com as necessidades (e o gesto que sai na hora, se houver).
@@ -100,6 +101,8 @@ class Vida:
self.alguem_perto = lambda: False # PIR/movimento recente self.alguem_perto = lambda: False # PIR/movimento recente
self.bloqueado = lambda: False # E-Stop ou modo presença com ninguém em casa self.bloqueado = lambda: False # E-Stop ou modo presença com ninguém em casa
self.pode_gemini = lambda: bool(os.environ.get('GEMINI_API_KEY')) self.pode_gemini = lambda: bool(os.environ.get('GEMINI_API_KEY'))
self.madrugada = self.noite_padrao # o main troca pelo horário dos ajustes
self.interagiu_em = 0.0
agora = time.time() agora = time.time()
self.config = dict(PADRAO) self.config = dict(PADRAO)
@@ -251,6 +254,8 @@ class Vida:
self.necessidades[k] = _limitar(self.necessidades[k] + v) self.necessidades[k] = _limitar(self.necessidades[k] + v)
if tipo in CUIDADOS: if tipo in CUIDADOS:
self.anotar('cuidado', CUIDADOS[tipo], agora) self.anotar('cuidado', CUIDADOS[tipo], agora)
if tipo in CUIDADOS or tipo == 'conversa':
self.interagiu_em = agora # alguém quis ele acordado (sensor não conta)
if gesto and not self.bloqueado(): if gesto and not self.bloqueado():
if self.dormindo(): if self.dormindo():
self.acordar(agora, None) self.acordar(agora, None)
@@ -271,7 +276,8 @@ class Vida:
if motivo: if motivo:
self.anotar('sono', motivo, agora) self.anotar('sono', motivo, agora)
def noite(self, agora): @staticmethod
def noite_padrao(agora):
hora = time.localtime(agora).tm_hour hora = time.localtime(agora).tm_hour
ini, fim = NOITE ini, fim = NOITE
return hora >= ini or hora < fim return hora >= ini or hora < fim
@@ -340,12 +346,12 @@ class Vida:
n = self.necessidades n = self.necessidades
perto = self.alguem_perto() perto = self.alguem_perto()
if self.dormindo(): if self.dormindo():
# acorda descansado de dia, ou sozinho quando a energia enche e tem alguém por perto # de madrugada nunca acorda sozinho (nem com gente se mexendo na cama); de dia, quando descansou
if n['energia'] >= 95 and (not self.noite(agora) or perto): if n['energia'] >= 95 and not self.madrugada(agora):
self.acordar(agora, 'Acordou descansado') self.acordar(agora, 'Acordou descansado')
return 'acordar' return 'acordar'
return None return None
if self.config['noite'] and self.noite(agora) and not perto: if self.config['noite'] and self.madrugada(agora) and agora - self.interagiu_em > PEDIU_ATENCAO:
self.dormir(agora, 'Ficou tarde, foi dormir') self.dormir(agora, 'Ficou tarde, foi dormir')
return 'dormir' return 'dormir'
if n['energia'] < 20: if n['energia'] < 20:
+16 -1
View File
@@ -55,7 +55,7 @@ export const GRUPOS: { titulo: string; descricao: string; campos: Campo[] }[] =
{ chave: 'economia', rotulo: 'Reduzir brilho em repouso', tipo: 'toggle' }, { chave: 'economia', rotulo: 'Reduzir brilho em repouso', tipo: 'toggle' },
{ chave: 'brilhoRepouso', rotulo: 'Brilho em repouso', tipo: 'slider', min: 0.01, max: 1, passo: 0.01 }, { chave: 'brilhoRepouso', rotulo: 'Brilho em repouso', tipo: 'slider', min: 0.01, max: 1, passo: 0.01 },
{ chave: 'repousoTela', rotulo: 'Espera para reduzir brilho', tipo: 'slider', min: 5, max: 300, passo: 5, unidade: ' s' }, { chave: 'repousoTela', rotulo: 'Espera para reduzir brilho', tipo: 'slider', min: 5, max: 300, passo: 5, unidade: ' s' },
{ chave: 'apagarTela', rotulo: 'Apagar a tela quando dormir', ajuda: 'Tela preta e brilho mínimo; acorda com som, movimento, toque ou PIR', tipo: 'toggle' }, { chave: 'apagarTela', rotulo: 'Apagar a tela quando dormir', ajuda: 'Tela preta e brilho mínimo; acorda com som repetido, movimento, toque ou PIR', tipo: 'toggle' },
{ chave: 'apagarTelaMin', rotulo: 'Dormindo há quanto tempo para apagar', tipo: 'slider', min: 1, max: 120, passo: 1, unidade: ' min' }, { chave: 'apagarTelaMin', rotulo: 'Dormindo há quanto tempo para apagar', tipo: 'slider', min: 1, max: 120, passo: 1, unidade: ' min' },
], ],
}, },
@@ -73,6 +73,21 @@ export const GRUPOS: { titulo: string; descricao: string; campos: Campo[] }[] =
{ chave: 'movolhar', rotulo: 'Quanto os olhos acompanham', tipo: 'slider', min: 0, max: 100, passo: 1, unidade: '%' }, { chave: 'movolhar', rotulo: 'Quanto os olhos acompanham', tipo: 'slider', min: 0, max: 100, passo: 1, unidade: '%' },
], ],
}, },
{
titulo: 'Sono e madrugada',
descricao: 'O que acorda o rosto enquanto ele dorme.',
campos: [
{ chave: 'somAcordar', rotulo: 'Sons para acordar', ajuda: 'Quantos barulhos em 1 minuto para ele acordar', tipo: 'slider', min: 1, max: 10, passo: 1 },
{
chave: 'noiteSilenciosa',
rotulo: 'Madrugada silenciosa',
ajuda: 'Movimento, câmera e PIR não acordam; só barulho alto repetido, toque ou conversa',
tipo: 'toggle',
},
{ chave: 'noiteInicio', rotulo: 'Começa às', tipo: 'slider', min: 0, max: 23, passo: 1, unidade: 'h' },
{ chave: 'noiteFim', rotulo: 'Termina às', tipo: 'slider', min: 0, max: 23, passo: 1, unidade: 'h' },
],
},
{ {
titulo: 'Vídeo e rastreamento', titulo: 'Vídeo e rastreamento',
descricao: 'Qualidade da transmissão e rastreamento feito neste PC.', descricao: 'Qualidade da transmissão e rastreamento feito neste PC.',
+7 -1
View File
@@ -30,6 +30,12 @@
function idade(d: number) { function idade(d: number) {
return d === 0 ? 'nasceu hoje' : d === 1 ? '1 dia de vida' : `${d} dias de vida`; return d === 0 ? 'nasceu hoje' : d === 1 ? '1 dia de vida' : `${d} dias de vida`;
} }
const horarioNoite = $derived.by(() => {
const c = estado.appConfig;
const ini = typeof c?.noiteInicio === 'number' ? c.noiteInicio : 23;
const fim = typeof c?.noiteFim === 'number' ? c.noiteFim : 7;
return `Das ${ini}h às ${fim}h. Gente se mexendo não acorda; carinho e conversa sim`;
});
const detalheFigurinhas = $derived( const detalheFigurinhas = $derived(
v ? [`${v.emojis_hoje} de ${v.config.max_emojis_dia} hoje`, MOTIVOS[v.pode_criar] ?? v.pode_criar].filter(Boolean).join(' · ') : '', v ? [`${v.emojis_hoje} de ${v.config.max_emojis_dia} hoje`, MOTIVOS[v.pode_criar] ?? v.pode_criar].filter(Boolean).join(' · ') : '',
); );
@@ -104,7 +110,7 @@
</span> </span>
{/snippet} {/snippet}
</Linha> </Linha>
<Linha icone={Moon} cor="#5e5ce6" rotulo="Dormir à noite" detalhe="Das 23h às 7h, se não tiver ninguém por perto"> <Linha icone={Moon} cor="#5e5ce6" rotulo="Dormir à noite" detalhe={horarioNoite}>
{#snippet acessorio()} {#snippet acessorio()}
<Interruptor rotulo="Dormir à noite" checked={v.config.noite} onchange={(l) => configurarVida({ noite: l })} /> <Interruptor rotulo="Dormir à noite" checked={v.config.noite} onchange={(l) => configurarVida({ noite: l })} />
{/snippet} {/snippet}