feat(vida): horário de dormir acompanha a rotina da casa e lição honesta
- A lição da rotina separa o que cai na madrugada: "das 10h às 23h vou
estar acordado; das 23h à 1h é a minha hora de dormir, fico quietinho
a não ser que alguém me chame" (antes prometia ficar acordado de
madrugada, o que nunca acontecia).
- Com 5+ dias de rotina, na virada do dia ele mexe sozinho no horário
da madrugada: atrasa o início se a primeira hora costuma ter gente,
adianta o fim se a casa acorda cedo; 1 h por noite, no máximo 3 h
depois / 2 h antes do que a pessoa escolheu, nunca menos de 5 h de
madrugada, e volta quando as horas esvaziam. Mudança à mão vira a
nova base. Fica no diário e no "Dia a dia"; liga/desliga no cartão.
- Faixas que passam da meia-noite viram uma só ("das 23h à 1h").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+94
-6
@@ -24,7 +24,7 @@ from pathlib import Path
|
||||
|
||||
from comum import gravar_atomico, ler_json
|
||||
|
||||
PADRAO = dict(ligado=True, analise_ia=True, ia='gemini')
|
||||
PADRAO = dict(ligado=True, analise_ia=True, ia='gemini', ajustar_sono=True)
|
||||
IAS = ('gemini', 'local', 's10')
|
||||
JANELA = 60 # s depois de um ato em que a resposta de alguém conta para ele
|
||||
PESO_RESPOSTA = {'carinho': 1.0, 'brincar': 1.0, 'conversa': 1.0, 'alimentar': 0.6, 'cutucar': 0.5}
|
||||
@@ -38,6 +38,9 @@ MIN_MINUTOS_HORA = 10 # minutos observados numa hora para ela entrar na rotin
|
||||
ROTINA_CHEIA, ROTINA_VAZIA = 0.5, 0.3
|
||||
AVISO_CHEGADA = 15 * 60 # s antes da hora movimentada em que ele acorda do cochilo
|
||||
MAX_LICOES = 30
|
||||
# Horário de dormir pela rotina: anda 1 h por noite, no máximo 3 h depois / 2 h antes do que a pessoa
|
||||
# escolheu, com pelo menos 5 dias observados, e a madrugada nunca fica menor que 5 h.
|
||||
SONO_MIN_DIAS, SONO_ATRASO_MAX, SONO_ADIANTO_MAX, SONO_MIN_HORAS = 5, 3, 2, 5
|
||||
MAX_DECISOES = 800 # decisões guardadas (com o motivo), umas duas semanas
|
||||
AVISO_SONO = 10 * 60 # no máximo uma nota de "não acordou com o sensor" a cada 10 min
|
||||
DIAS_SEMANA = ['segunda', 'terça', 'quarta', 'quinta', 'sexta', 'sábado', 'domingo']
|
||||
@@ -87,11 +90,17 @@ def _faixas(horas):
|
||||
faixas[-1] = (faixas[-1][0], h + 1)
|
||||
else:
|
||||
faixas.append((h, h + 1))
|
||||
if len(faixas) > 1 and faixas[0][0] == 0 and faixas[-1][1] == 24: # passa da meia-noite: uma faixa só
|
||||
faixas = [(faixas[-1][0], faixas[0][1] + 24)] + faixas[1:-1]
|
||||
return faixas
|
||||
|
||||
|
||||
def _na_noite(h, ini, fim):
|
||||
return ini <= h < fim if ini < fim else (h >= ini or h < fim)
|
||||
|
||||
|
||||
def _texto_faixas(faixas):
|
||||
partes = ['das %dh às %dh' % (a, b % 24) for a, b in faixas]
|
||||
partes = ['%s %dh %s %dh' % ('da' if a == 1 else 'das', a, 'à' if b % 24 == 1 else 'às', b % 24) for a, b in faixas]
|
||||
return partes[0] if len(partes) == 1 else ', '.join(partes[:-1]) + ' e ' + partes[-1]
|
||||
|
||||
|
||||
@@ -126,6 +135,11 @@ class Aprendizado:
|
||||
self.pode_ia = lambda provedor: False
|
||||
self.decisoes = [] # [{horario, tipo, texto, motivo}] mais nova primeiro
|
||||
self.sono_avisado = 0.0
|
||||
# horário da madrugada (Ajustes do celular): o main liga; None = madrugada silenciosa desligada
|
||||
self.noite = lambda: None
|
||||
self.mudar_noite = lambda ini, fim: None
|
||||
self.sono_base = None # (início, fim) que a pessoa escolheu
|
||||
self.sono_auto = None # (início, fim) que ele mesmo pôs por último
|
||||
try:
|
||||
salvo = json.loads(self.caminho.read_text(encoding='utf-8'))
|
||||
self.config.update(self.validar(salvo.get('config', {})))
|
||||
@@ -151,6 +165,9 @@ class Aprendizado:
|
||||
self.analise['em'] = salvo['analise_em']
|
||||
if isinstance(salvo.get('hoje'), dict) and salvo['hoje'].get('dia'):
|
||||
self.hoje.update(salvo['hoje'])
|
||||
for chave in ('sono_base', 'sono_auto'):
|
||||
if isinstance(salvo.get(chave), list) and len(salvo[chave]) == 2:
|
||||
setattr(self, chave, tuple(int(v) % 24 for v in salvo[chave]))
|
||||
except (OSError, ValueError, TypeError, KeyError, AttributeError):
|
||||
pass
|
||||
|
||||
@@ -200,7 +217,8 @@ class Aprendizado:
|
||||
licoes=self.licoes, rotina_avisada=self.rotina_avisada, rotina_avisada_dia=self.rotina_avisada_dia,
|
||||
hoje=self.hoje, semana=self.semana,
|
||||
semana_n=self.semana_n, historico=self.historico, plano=self.plano, sugestao=self.sugestao,
|
||||
analise_em=self.analise['em'], decisoes=self.decisoes)
|
||||
analise_em=self.analise['em'], decisoes=self.decisoes, sono_base=self.sono_base,
|
||||
sono_auto=self.sono_auto)
|
||||
self.salvo_em = time.time()
|
||||
try:
|
||||
gravar_atomico(self.caminho, dados)
|
||||
@@ -436,14 +454,27 @@ class Aprendizado:
|
||||
licoes.append('Tentei %s %d vezes e ninguém ligou. Vou fazer menos.' % (self.nome(ignorado[0][1]), ignorado[0][0]))
|
||||
if observou and hoje['atos'] >= 5 and not sum(hoje['respostas'].values()):
|
||||
licoes.append('Hoje ninguém brincou comigo. Amanhã eu tento coisas diferentes.')
|
||||
# rotina: quando passa a ter um padrão (ou ele muda), conta
|
||||
# sono: a rotina mexe no horário de dormir (antes de contar a rotina, que já usa o novo horário)
|
||||
licoes += self.ajustar_noite(hoje['dia'])
|
||||
# rotina: quando passa a ter um padrão (ou ele muda), conta, separando o que cai na madrugada
|
||||
if self.dias >= 2 and not self.plano:
|
||||
cheias = [h for h in range(24) if self.rotina[h] is not None and self.rotina[h] >= ROTINA_CHEIA]
|
||||
mudou = not self.rotina_avisada or len(set(cheias) ^ set(self.rotina_avisada)) >= 3 or (
|
||||
cheias != self.rotina_avisada and self.dias - self.rotina_avisada_dia >= 7)
|
||||
if cheias and mudou:
|
||||
licoes.append('Percebi que vocês costumam estar por perto %s. Vou estar acordado nessa hora.'
|
||||
% _texto_faixas(_faixas(cheias)))
|
||||
noite = self.noite()
|
||||
dormindo = [h for h in cheias if noite and _na_noite(h, *noite)]
|
||||
acordado = [h for h in cheias if h not in dormindo]
|
||||
quieto = 'é a minha hora de dormir: fico quietinho, a não ser que alguém me chame.'
|
||||
if acordado:
|
||||
texto = ('Percebi que vocês costumam estar por perto %s. Vou estar acordado nessa hora.'
|
||||
% _texto_faixas(_faixas(acordado)))
|
||||
if dormindo:
|
||||
resto = _texto_faixas(_faixas(dormindo)) + ' ' + quieto
|
||||
texto += ' ' + resto[0].upper() + resto[1:]
|
||||
else:
|
||||
texto = 'Percebi gente por perto %s, mas %s' % (_texto_faixas(_faixas(dormindo)), quieto)
|
||||
licoes.append(texto)
|
||||
self.rotina_avisada, self.rotina_avisada_dia = cheias, self.dias
|
||||
# sono: acordou à toa várias vezes na mesma hora
|
||||
piores = sorted(((n, h) for h, n in enumerate(hoje['falsos']) if n >= 2), reverse=True)
|
||||
@@ -463,6 +494,62 @@ class Aprendizado:
|
||||
del self.licoes[MAX_LICOES:]
|
||||
return licoes
|
||||
|
||||
def ajustar_noite(self, dia):
|
||||
"""Muda o horário da madrugada aos poucos pela rotina da casa. Devolve as lições."""
|
||||
noite = self.noite()
|
||||
if not noite or not self.config['ajustar_sono'] or not self.ligado():
|
||||
return []
|
||||
if self.sono_base is None or noite != self.sono_auto:
|
||||
self.sono_base = noite # a pessoa escolheu (ou mudou) o horário: vira a base
|
||||
if self.dias < SONO_MIN_DIAS:
|
||||
return []
|
||||
ini, fim = noite
|
||||
base_ini, base_fim = self.sono_base
|
||||
|
||||
def cheia(h):
|
||||
v = self.rotina[h % 24]
|
||||
return v is not None and v >= ROTINA_CHEIA
|
||||
|
||||
def vazia(h):
|
||||
v = self.rotina[h % 24]
|
||||
return v is not None and v <= ROTINA_VAZIA
|
||||
|
||||
atraso, adianto = (ini - base_ini) % 24, (base_fim - fim) % 24
|
||||
novo_ini, novo_fim = ini, fim
|
||||
if cheia(ini) and atraso < SONO_ATRASO_MAX:
|
||||
novo_ini = (ini + 1) % 24 # a primeira hora da madrugada costuma ter gente
|
||||
elif 0 < atraso <= SONO_ATRASO_MAX and vazia(ini - 1):
|
||||
novo_ini = (ini - 1) % 24 # a casa voltou a dormir cedo: volta para o de antes
|
||||
if cheia(fim - 1) and adianto < SONO_ADIANTO_MAX:
|
||||
novo_fim = (fim - 1) % 24 # a casa acorda antes
|
||||
elif 0 < adianto <= SONO_ADIANTO_MAX and vazia(fim):
|
||||
novo_fim = (fim + 1) % 24
|
||||
if (novo_ini, novo_fim) == tuple(noite) or (novo_fim - novo_ini) % 24 < SONO_MIN_HORAS:
|
||||
return []
|
||||
self.mudar_noite(novo_ini, novo_fim)
|
||||
self.sono_auto = (novo_ini, novo_fim)
|
||||
licoes, motivos = [], []
|
||||
if novo_ini != ini:
|
||||
if novo_ini == (ini + 1) % 24:
|
||||
licoes.append('Vocês têm ficado acordados até mais tarde: agora eu vou dormir às %dh (antes era %dh).'
|
||||
% (novo_ini, ini))
|
||||
motivos.append('às %dh costuma ter gente' % ini)
|
||||
else:
|
||||
licoes.append('A casa voltou a dormir mais cedo: volto a dormir às %dh.' % novo_ini)
|
||||
motivos.append('às %dh a casa costuma estar vazia' % novo_ini)
|
||||
if novo_fim != fim:
|
||||
if novo_fim == (fim - 1) % 24:
|
||||
licoes.append('Vocês têm acordado cedo: agora eu acordo às %dh (antes era %dh).' % (novo_fim, fim))
|
||||
motivos.append('às %dh costuma ter gente' % novo_fim)
|
||||
else:
|
||||
licoes.append('A casa voltou a acordar mais tarde: volto a acordar às %dh.' % novo_fim)
|
||||
motivos.append('às %dh a casa costuma estar vazia' % fim)
|
||||
fim_do_dia = time.mktime(time.strptime(dia + ' 23:59:59', '%Y-%m-%d %H:%M:%S'))
|
||||
self.decisoes.insert(0, dict(
|
||||
horario=fim_do_dia, tipo='sono', texto='Mudou o horário de dormir para %dh às %dh' % (novo_ini, novo_fim),
|
||||
motivo='; '.join(motivos) + ' (o horário escolhido é %dh às %dh)' % tuple(self.sono_base)))
|
||||
return licoes
|
||||
|
||||
# ------------------------------------------------------------------ semana: IA e plano
|
||||
@staticmethod
|
||||
def validar_plano(bruto, origem, criado=None):
|
||||
@@ -691,6 +778,7 @@ class Aprendizado:
|
||||
gosto.sort(key=lambda g: -g['peso'])
|
||||
hoje = self.hoje
|
||||
return dict(config=dict(self.config), dias=self.dias, rotina=list(self.rotina),
|
||||
sono_base=self.sono_base, sono_auto=self.sono_auto,
|
||||
sono=[self._extra(f) if self.ligado() else 0 for f in self.falsos],
|
||||
gosto=gosto, licoes=self.licoes[:12],
|
||||
semana=[[v if self.semana_n[s][h] else None for h, v in enumerate(linha)]
|
||||
|
||||
@@ -565,6 +565,16 @@ def main():
|
||||
# a análise da semana usa a mesma IA do estúdio (Gemini, IA local ou o S10)
|
||||
aprendizado.ia = servidor.criacao.chamar
|
||||
aprendizado.pode_ia = lambda provedor: bool(servidor.criacao.provedores().get(provedor, {}).get('pronto'))
|
||||
|
||||
# a rotina da casa pode mexer no horário da madrugada (o mesmo dos Ajustes do celular)
|
||||
def noite_atual():
|
||||
c = servidor.controle.config
|
||||
return (int(c.get('noiteInicio', 23)), int(c.get('noiteFim', 7))) if c.get('noiteSilenciosa', True) else None
|
||||
|
||||
def mudar_noite(ini, fim):
|
||||
servidor.controle.publicar('app_config', servidor.controle.atualizar({'noiteInicio': ini, 'noiteFim': fim}))
|
||||
aprendizado.noite = noite_atual
|
||||
aprendizado.mudar_noite = mudar_noite
|
||||
# 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"
|
||||
|
||||
|
||||
@@ -136,6 +136,78 @@ class AprendizadoTest(unittest.TestCase):
|
||||
self.assertEqual(outro.peso('rir'), 1.0)
|
||||
self.assertEqual(Aprendizado(self.caminho).itens, {})
|
||||
|
||||
def ligar_noite(self, ini=23, fim=7):
|
||||
estado = {'noite': (ini, fim)}
|
||||
self.a.noite = lambda: estado['noite']
|
||||
self.a.mudar_noite = lambda i, f: estado.update(noite=(i, f))
|
||||
return estado
|
||||
|
||||
def test_rotina_separa_o_que_cai_na_madrugada(self):
|
||||
self.ligar_noite(23, 7)
|
||||
self.a.dias = 3
|
||||
for h in list(range(10, 24)) + [0, 2]:
|
||||
self.a.rotina[h] = 0.8
|
||||
self.a.config['ajustar_sono'] = False
|
||||
licoes = self.a.fechar_dia()
|
||||
texto = next(t for t in licoes if t.startswith('Percebi'))
|
||||
self.assertIn('das 10h às 23h. Vou estar acordado nessa hora.', texto)
|
||||
self.assertIn('Das 23h à 1h e das 2h às 3h é a minha hora de dormir', texto)
|
||||
# só gente na madrugada: não promete ficar acordado
|
||||
self.a.rotina = [None] * 24
|
||||
self.a.rotina[2], self.a.rotina_avisada = 0.9, []
|
||||
texto = next(t for t in self.a.fechar_dia() if t.startswith('Percebi'))
|
||||
self.assertEqual(texto, 'Percebi gente por perto das 2h às 3h, mas é a minha hora de dormir: '
|
||||
'fico quietinho, a não ser que alguém me chame.')
|
||||
|
||||
def test_horario_de_dormir_acompanha_a_casa_aos_poucos_e_volta(self):
|
||||
estado = self.ligar_noite(23, 7)
|
||||
self.a.dias = 2
|
||||
self.a.rotina[23], self.a.rotina[0], self.a.rotina[1] = 0.8, 0.7, 0.7
|
||||
self.assertEqual(self.a.ajustar_noite('2026-10-06'), []) # poucos dias: ainda não mexe
|
||||
self.a.dias = 6
|
||||
licoes = self.a.ajustar_noite('2026-10-06')
|
||||
self.assertEqual(estado['noite'], (0, 7)) # 1 h por noite
|
||||
self.assertIn('agora eu vou dormir às 0h (antes era 23h)', licoes[0])
|
||||
self.assertEqual(self.a.decisoes[0]['tipo'], 'sono')
|
||||
self.a.ajustar_noite('2026-10-07')
|
||||
self.a.ajustar_noite('2026-10-08')
|
||||
self.assertEqual(estado['noite'], (2, 7)) # 1h tem gente, 2h já não conta
|
||||
self.a.rotina[2] = 0.9
|
||||
self.a.ajustar_noite('2026-10-09')
|
||||
self.assertEqual(estado['noite'], (2, 7)) # no máximo 3 h depois do escolhido
|
||||
# a casa voltou a dormir cedo: volta 1 h por noite até o horário escolhido
|
||||
self.a.rotina[1] = self.a.rotina[0] = self.a.rotina[23] = 0.1
|
||||
self.assertIn('volto a dormir às 1h', self.a.ajustar_noite('2026-10-10')[0])
|
||||
self.a.ajustar_noite('2026-10-11')
|
||||
self.a.ajustar_noite('2026-10-12')
|
||||
self.assertEqual(estado['noite'], (23, 7))
|
||||
self.assertEqual(self.a.ajustar_noite('2026-10-13'), [])
|
||||
|
||||
def test_acordar_mais_cedo_limites_e_horario_escolhido_a_mao(self):
|
||||
estado = self.ligar_noite(23, 7)
|
||||
self.a.dias = 6
|
||||
self.a.rotina[6], self.a.rotina[5] = 0.8, 0.8
|
||||
self.a.ajustar_noite('2026-10-06')
|
||||
self.a.ajustar_noite('2026-10-07')
|
||||
self.a.ajustar_noite('2026-10-08')
|
||||
self.assertEqual(estado['noite'], (23, 5)) # no máximo 2 h antes
|
||||
# a pessoa muda o horário à mão: vira a nova base e ele não desfaz
|
||||
estado['noite'] = (22, 8)
|
||||
self.a.rotina = [None] * 24
|
||||
self.assertEqual(self.a.ajustar_noite('2026-10-09'), [])
|
||||
self.assertEqual(self.a.sono_base, (22, 8))
|
||||
# madrugada nunca fica menor que 5 h
|
||||
estado['noite'] = (2, 7)
|
||||
self.a.rotina[2], self.a.rotina[6] = 0.9, 0.9
|
||||
self.a.ajustar_noite('2026-10-10')
|
||||
self.assertEqual(estado['noite'], (2, 7))
|
||||
# desligado ou sem madrugada: não mexe
|
||||
self.a.config['ajustar_sono'] = False
|
||||
self.assertEqual(self.a.ajustar_noite('2026-10-11'), [])
|
||||
self.a.config['ajustar_sono'] = True
|
||||
self.a.noite = lambda: None
|
||||
self.assertEqual(self.a.ajustar_noite('2026-10-12'), [])
|
||||
|
||||
def test_validar(self):
|
||||
with self.assertRaises(ValueError):
|
||||
self.a.configurar(dict(ligado='sim'))
|
||||
|
||||
@@ -81,6 +81,14 @@
|
||||
vista = 'plano';
|
||||
}
|
||||
}
|
||||
// horário de dormir: o que a pessoa escolheu e o que ele ajustou pela rotina da casa
|
||||
const sonoTexto = $derived.by(() => {
|
||||
if (!a) return '';
|
||||
if (!(a.config.ajustar_sono ?? true)) return 'Desligado: dorme sempre no horário dos Ajustes';
|
||||
const b = a.sono_base, x = a.sono_auto;
|
||||
if (b && x && (b[0] !== x[0] || b[1] !== x[1])) return `Dorme das ${x[0]}h às ${x[1]}h pela rotina (você escolheu ${b[0]}h às ${b[1]}h)`;
|
||||
return 'Com 5 dias de rotina, atrasa ou adianta até 3 h / 2 h, uma hora por noite';
|
||||
});
|
||||
</script>
|
||||
|
||||
<Grupo
|
||||
@@ -148,6 +156,10 @@
|
||||
{:else if a.sono.some((n) => n > 0)}
|
||||
<p class="nota">☾ Em algumas horas ele acordou à toa: ali o sensor precisa de mais movimento para acordá-lo.</p>
|
||||
{/if}
|
||||
<div class="ajustes-ia">
|
||||
<Interruptor rotulo="Ajustar o horário de dormir pela rotina" checked={a.config.ajustar_sono ?? true} onchange={(l) => configurarAprendizado({ ajustar_sono: l })} />
|
||||
<span>{sonoTexto}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="bloco" aria-labelledby="ap-rotina">
|
||||
|
||||
@@ -120,7 +120,9 @@ export interface PlanoSemana {
|
||||
}
|
||||
|
||||
export interface Aprendizado {
|
||||
config: { ligado: boolean; analise_ia: boolean; ia: 'gemini' | 'local' | 's10' };
|
||||
config: { ligado: boolean; analise_ia: boolean; ia: 'gemini' | 'local' | 's10'; ajustar_sono?: boolean };
|
||||
sono_base?: [number, number] | null;
|
||||
sono_auto?: [number, number] | null;
|
||||
/** 7 dias × 24 horas: chance de ter gente por perto (null = sem dados). */
|
||||
semana: (number | null)[][];
|
||||
historico_dias: number;
|
||||
|
||||
Reference in New Issue
Block a user