merged loopaudio

This commit is contained in:
Your Name 2019-05-11 15:42:41 +02:00
commit edd6d8924a
10 changed files with 196 additions and 109 deletions

View File

@ -93,7 +93,7 @@ chown=pi:pi
## Deploy / usefull commands
```bash
for i in {1..6}; do rsync -av ~/hugvey/ pi@hugvey$i.local:/home/pi/hugvey/ --exclude=www --exclude=venv --exclude=local --exclude=*.pyc --exclude=.git; done
for i in {1..26}; do echo $i; rsync -av ~/hugvey/ pi@hugvey$i.local:/home/pi/hugvey/ --exclude=www --exclude=venv --exclude=local --exclude=*.pyc --exclude=.git --exclude=recordings --exclude=/voice* --exclude=/pd; done
```
```bash
@ -204,4 +204,4 @@ times occured/only on n-th instance: determines the order of diversions of the s
## 4G Modem
Visit 192.168.5.1
The password is at the bottom of the device.
The password is at the bottom of the device.

View File

@ -5,7 +5,7 @@ voice:
input_rate: 44100
target_rate: 16000
port: 4444
input_name: 'AK5371'
input_name: 'USB Audio Device'
output_name: 'USB Audio Device'
input_mixer: 'Mic'
output_mixer: 'PCM'
@ -13,6 +13,3 @@ voice:
output_volume: 30
file_address: "http://hugveycmd.local:8888"
output_driver: pulseaudio

View File

@ -24,6 +24,7 @@ from hugvey.voice import VoiceStorage
import multiprocessing
from hugvey.speech.recorder import Recorder
from pythonosc import udp_client
import copy
mainLogger = logging.getLogger("hugvey")
@ -67,7 +68,7 @@ class CentralCommand(object):
self.languageFiles = {}
self.languageConfig = {}
self.args = args # cli args
eventLogger.addHandler(logging.handlers.QueueHandler(self.logQueue))
def loadConfig(self, filename):
@ -85,14 +86,14 @@ class CentralCommand(object):
self.hugvey_ids = [i + 1 for i in range(self.config['hugveys'])]
self.loadLanguages()
voice_dir = os.path.join(self.config['web']['files_dir'], 'voices')
self.voiceStorage = VoiceStorage(voice_dir, self.languageConfig)
self.panopticon = Panopticon(self, self.config, self.voiceStorage)
def loadLanguages(self):
logger.debug('load language files')
self.languages = {}
@ -116,16 +117,17 @@ class CentralCommand(object):
# if not hv.story:
# status['status'] = 'off'
# return status
status['status'] = hv.getStatus()
status['language'] = hv.language_code
status['light_id'] = hv.lightId
status['msg'] = hv.story.currentMessage.id if hv.story and hv.story.currentMessage else None
# status['finished'] = hv.story.isFinished()
status['history'] = {} if isSelected is False or not hv.story else hv.story.getLogSummary()
# status['history'] = hv.story.getLogSummary() # disabled as it is a bit slow. We now have eventLog
# status['counts'] = {t: len(a) for t, a in status['history'].items() if t != 'directions' }
status['counts'] = {} if not hv.story else hv.story.getLogCounts()
status['duration'] = 0 if not hv.story else hv.story.timer.getElapsed()
status['duration'] = 0 if not hv.story else hv.story.timer.getElapsed()
return status
@ -138,10 +140,10 @@ class CentralCommand(object):
'logbookId': None,
'logbook': [],
}
#use this to test if any threads stay open
# eg. after killing/dying of a hugvey
# print(threading.enumerate())
# print(threading.enumerate())
for hv_id in self.hugvey_ids:
status['hugveys'].append(self.getHugveyStatus(hv_id, selected_id == hv_id))
@ -150,7 +152,7 @@ class CentralCommand(object):
if self.hugveys[selected_id].recorder:
status['logbook'] = self.hugveys[selected_id].recorder.currentLog
status['logbookId'] = selected_id
return status
def commandHugvey(self, hv_id, msg):
@ -168,8 +170,8 @@ class CentralCommand(object):
def _queueCommand(self, hv_id, msg):
self.commandQueue.put_nowait((hv_id, msg))
def commandLight(self, route, data):
"""
Buffer light commands
@ -214,13 +216,13 @@ class CentralCommand(object):
logger.warn('Stopping command sender')
s.close()
async def lightSender(self):
lightConn = udp_client.SimpleUDPClient(
self.config['light']['ip'],
self.config['light']['port'])
logger.info(f"Ready to send light commands to: {self.config['light']['ip']}:{self.config['light']['port']}")
while self.isRunning.is_set():
@ -232,6 +234,23 @@ class CentralCommand(object):
logger.warn('Stopping light sender')
lightConn._sock.close()
async def redLightController(self):
"""
Every second, check if no hugveys are available. If so, the red light should be
overruled to be on. If any is available, send a 0 to release the override.
"""
currentCode = None
while self.isRunning.is_set():
statusses = [hv.getStatus() for hv in self.hugveys.values()]
lightOn = HugveyState.STATE_AVAILABLE not in statusses
lightCode = 1 if lightOn else 0
if lightCode != currentCode:
self.commandLight('/red', [lightCode])
currentCode = lightCode
await asyncio.sleep(1)
logger.warn('Stopping red light controller')
def instantiateHugvey(self, hugvey_id):
'''
Start a HugveyState, according to a show_yourself reply
@ -246,7 +265,7 @@ class CentralCommand(object):
thread = threading.Thread(
target=self.hugveyStateRunner, args=(hugvey_id,), name=f"hugvey#{hugvey_id}")
thread.start()
def hugveyStateRunner(self, hugvey_id):
while self.isRunning.is_set():
logger.info(f'Instantiate hugvey #{hugvey_id}')
@ -260,7 +279,7 @@ class CentralCommand(object):
return
logger.critical(f'Hugvey stopped (crashed?). Reinstantiate after 5 sec')
time.sleep(5)
async def timerEmitter(self):
"""
This is fixed: a one hour loop with a collective moment 10-15 minutes,
@ -270,25 +289,25 @@ class CentralCommand(object):
intervals = [
{
'start_time': 10*60,
'duration': 5 * 60,
'duration': 5 * 60,
},
{
'start_time': 30*60,
'duration': 5 * 60,
'duration': 5 * 60,
},
{
'start_time': 50*60,
'duration': 5 * 60,
'duration': 5 * 60,
}
]
self.start_time = time.time()
# TODO: emit start event
while self.isRunning.is_set():
pass
async def eventListener(self):
s = self.ctx.socket(zmq.SUB)
s.bind(self.config['events']['listen_address'])
@ -301,7 +320,7 @@ class CentralCommand(object):
while self.isRunning.is_set():
try:
hugvey_id, msg = await zmqReceive(s)
if hugvey_id not in self.hugvey_ids:
logger.critical(
"Message from alien Hugvey: {}".format(hugvey_id))
@ -337,7 +356,7 @@ class CentralCommand(object):
fn = await self.voiceStorage.requestFile(hv.language_code, text, isVariable)
if fn is None:
eventLogger.getChild(f"{hugvey_id}").critical("error: No voice file fetched, check logs.")
fn = 'local/crash.wav'
fn = 'local/crash.wav'
# TODO: trigger a repeat/crash event.
await s.send_string(fn)
except Exception as e:
@ -356,6 +375,9 @@ class CentralCommand(object):
self.catchException(self.commandSender()))
self.tasks['lightSender'] = self.loop.create_task(
self.catchException(self.lightSender()))
self.tasks['redLightController'] = self.loop.create_task(
self.catchException(self.redLightController()))
for hid in self.hugvey_ids:
self.tasks['voiceListener'] = self.loop.create_task(
self.catchException(self.voiceListener(hid)))
@ -365,12 +387,12 @@ class CentralCommand(object):
self.panopticon_thread = threading.Thread(
target=self.panopticon.start, name="Panopticon")
self.panopticon_thread.start()
self.loop.run_forever()
def stop(self):
self.isRunning.clear()
async def catchException(self, awaitable):
try:
# print(awaitable)
@ -384,7 +406,7 @@ class HugveyState(object):
"""Represents the state of a Hugvey client on the server.
Manages server connections & voice parsing etc.
"""
# all statusses can only go up or down, except for gone, which is an error state:
# off <-> blocked <-> available <-> running <-> paused
STATE_OFF = "off"
@ -396,13 +418,14 @@ class HugveyState(object):
def __init__(self, id: int, command: CentralCommand):
self.id = id
self.lightId = id
self.command = command
self.logger = mainLogger.getChild(f"{self.id}").getChild("command")
self.loop = asyncio.new_event_loop()
self.isConfigured = None
self.isRunning = asyncio.Event(loop=self.loop)
self.isRunning.clear()
self.eventQueue = None
self.language_code = 'en-GB'
self.story = None
@ -413,24 +436,24 @@ class HugveyState(object):
self.notShuttingDown = True # TODO: allow shutdown of object
self.startMsgId = None
self.eventLogger = eventLogger.getChild(f"{self.id}")
self.setStatus(self.STATE_GONE)
self.requireRestartAfterStop = None
def __del__(self):
self.logger.warn("Destroying hugvey object")
def getStatus(self):
return self.status
def setStatus(self, status):
self.status = status
lightOn = status in [self.STATE_AVAILABLE, self.STATE_PAUSE]
self.setLightStatus(lightOn)
self.eventLogger.info(f"status: {self.status}")
def config(self, hostname, ip):
self.ip = ip
self.hostname = hostname
@ -441,7 +464,7 @@ class HugveyState(object):
else:
self.logger.info(
f"Hugvey {self.id} at {self.ip}, host: {self.hostname}")
if self.status == self.STATE_GONE:
# turn on :-)
self.setStatus(self.STATE_BLOCKED)
@ -474,7 +497,7 @@ class HugveyState(object):
self.logger.exception(e)
self.logger.critical(f"Hugvey crash")
self.eventLogger.critical(f"error: {e}")
# restart
# TODO: test proper functioning
self.shutdown()
@ -488,18 +511,18 @@ class HugveyState(object):
else:
# Allow for both the Hugvey Command, or the Story handle the event.
self.loop.call_soon_threadsafe(self._queueEvent, msg)
def _queueEvent(self, msg):
"""
Put event in both the event loop for the story as well as the Hugvey State handler
"""
self.logger.debug(f"Queue event in hugvey loop: {msg}")
self.eventQueue.put_nowait(msg)
# connection events don't need to go to the story
if msg['event'] == 'connection':
return
if self.story:
self.story.events.append(msg)
else:
@ -516,7 +539,7 @@ class HugveyState(object):
self.logger.error("Hugvey did not send heartbeat.")
self.gone()
continue
self.logger.debug("Received: {}".format(event))
if event['event'] == 'connection':
# 'event': 'connection',
@ -524,11 +547,11 @@ class HugveyState(object):
# 'host': socket.gethostname(),
# 'ip': self.getIp(),
self.config(event['host'], event['ip'])
if event['event'] == 'language':
self.setLanguage(event['code'])
if event['event'] == 'pause':
self.pause()
if event['event'] == 'block':
@ -541,9 +564,11 @@ class HugveyState(object):
self.story._finish() # finish story AND hugvey state
if event['event'] == 'resume':
self.resume()
if event['event'] == 'change_language':
self.setLanguage(event['lang_code'])
if event['event'] == 'change_light':
self.setLightId(event['light_id'])
if event['event'] == 'play_msg':
self.logger.info(f"Play given message {event['msg_id']}")
if not self.story:
@ -553,7 +578,7 @@ class HugveyState(object):
# self.restart()
if self.story is None:
return
self.startMsgId = event['msg_id']
self.logger.debug(f"Restart from {self.startMsgId}")
self.restart()
@ -563,18 +588,18 @@ class HugveyState(object):
def setLanguage(self, language_code):
if language_code not in self.command.languages:
raise Exception("Invalid language {}".format(language_code))
self.logger.info(f"set language: {language_code}")
self.language_code = language_code
if self.google:
self.google.setLanguage(language_code)
if self.isRunning.is_set():
self.restart()
# self.story.reset()
# self.story.setStoryData(self.command.languages[language_code])
def pause(self):
self.logger.info('Pause')
if self.google:
@ -583,7 +608,7 @@ class HugveyState(object):
self.story.pause()
self.isRunning.clear()
self.setStatus(self.STATE_PAUSE)
def resume(self):
""" Start playing without reset"""
self.logger.info('Resume')
@ -593,14 +618,14 @@ class HugveyState(object):
self.story.resume()
self.isRunning.set()
self.setStatus(self.STATE_RUNNING)
def restart(self):
"""Start playing with reset"""
self.logger.info('Restart')
if self.story:
self.story.stop()
self.resume()
def block(self):
"""Block a hugvey"""
self.logger.info('block')
@ -610,31 +635,37 @@ class HugveyState(object):
self.story.finish()
self.isRunning.clear()
self.setStatus(self.STATE_BLOCKED)
def available(self):
"""Put in available mode"""
self.logger.info('Finish/Await')
self.pause()
self.setStatus(self.STATE_AVAILABLE)
def setLightStatus(self, on):
status = 1 if on else 0
self.logger.log(LOG_BS, f"Send /hugvey {status}")
self.command.commandLight('/hugvey', [self.id, status])
self.command.commandLight('/hugvey', [self.lightId, status])
def setLightId(self, id):
"""
Connect hugvey to another light
"""
self.lightId = id
def gone(self):
'''Status to 'gone' as in, shutdown/crashed/whatever
'''
self.pause()
if self.story:
self.story.stop()
self.logger.warn('Gone')
self.eventLogger.warn("Gone")
self.isConfigured = None
self.setStatus(self.STATE_GONE)
def shutdown(self, definitive = False):
self.logger.info(f"Start shutdown sequence {definitive}")
self.eventLogger.critical(f"error: shutting down")
@ -643,7 +674,7 @@ class HugveyState(object):
if self.story:
self.story.shutdown()
self.story = None
# shutdown for stream consumers already ran. Only clear references
if self.google:
self.google = None
@ -651,14 +682,14 @@ class HugveyState(object):
self.player = None
if self.recorder:
self.recorder = None
if self.requireRestartAfterStop is None:
# prevent double setting of the same variable
# first call sometimes triggers second
self.requireRestartAfterStop = not definitive
self.notShuttingDown = False
async def playStory(self):
while self.notShuttingDown:
@ -676,13 +707,13 @@ class HugveyState(object):
self.logger.warn(f"Starting from {startMsgId}")
if not self.streamer:
await asyncio.sleep(1)
self.streamer.triggerStart()
self.story.setStoryData(self.command.languages[self.language_code])
self.story.setStoryData(copy.deepcopy(self.command.languages[self.language_code]))
self.setLightStatus(False)
await self.story.run(startMsgId)
# self.story = None
def getStreamer(self):
if not self.streamer:
self.streamer = AudioStreamer(
@ -690,19 +721,19 @@ class HugveyState(object):
self.ip,
int(self.command.config['voice']['port']) + self.id,
self.id)
if self.command.config['voyeur']:
self.logger.warn("Debug on: Connecting Audio player")
self.player = Player(
self.command.config['voice']['src_rate'], self.command.config['voice']['out_rate'])
self.streamer.addConsumer(self.player)
if self.command.config['voice']['record_dir']:
self.logger.warn("Record Audio of conversation")
self.recorder = Recorder( self.id,
self.command.config['voice']['src_rate'], self.command.config['voice']['record_dir'])
self.streamer.addConsumer(self.recorder)
self.logger.debug("Start Speech")
self.google = GoogleVoiceClient(
hugvey=self,
@ -717,7 +748,7 @@ class HugveyState(object):
'''
Start the audio streamer service
'''
self.logger.debug("Start audio loop")
while self.notShuttingDown:

View File

@ -66,6 +66,8 @@ def getWebSocketHandler(central_command):
self.msgFinish(msg['hugvey'])
elif msg['action'] == 'change_language':
self.msgChangeLanguage(msg['hugvey'], msg['lang_code'])
elif msg['action'] == 'change_light':
self.msgChangeLightId(msg['hugvey'], int(msg['light_id']))
elif msg['action'] == 'play_msg':
self.msgPlayMsg(msg['hugvey'], msg['msg_id'])
else:
@ -119,6 +121,9 @@ def getWebSocketHandler(central_command):
def msgChangeLanguage(self, hv_id, lang_code):
central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'change_language', 'lang_code': lang_code})
def msgChangeLightId(self, hv_id, lightId):
central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'change_light', 'light_id': lightId})
def msgPlayMsg(self, hv_id, msg_id):
central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'play_msg', 'msg_id': msg_id})

View File

@ -692,16 +692,14 @@ class Diversion(object):
# if self.params['returnAfterStrand']:
# await story.setCurrentMessage(self.returnMessage)
async def _divergeIfReplyContains(self, story, msgFrom, msgTo, direction):
async def _divergeIfReplyContains(self, story, msgFrom, msgTo, _):
"""
Participant doesn't speak for x consecutive replies (has had timeout)
"""
':type story: Story'
# TODO: disable check on msgFrom/msgTo to allow for own timing (2 sec)
# use story.currentReply.getTimeSinceLastUtterance() > 2
if story.currentDiversion or not msgFrom or not msgTo:
if story.currentDiversion: # or not msgFrom or not msgTo:
# don't do nested diversions
# if we remove this, don't forget to double check 'returnMessage'
return False
if self.hasHit:
@ -711,6 +709,22 @@ class Diversion(object):
if story.currentReply is None or not self.regex:
return
direction = story.getDefaultDirectionForMsg(story.currentMessage)
if not direction:
# ignore the direction argument, and only check if the current message has a valid default
return
msgTo = direction.msgTo
if not direction:
return
waitTime = 1.8 if 'waitTime' not in self.params else float(self.params['waitTime'])
timeSince = story.currentReply.getTimeSinceLastUtterance()
if timeSince < waitTime:
story.logger.log(LOG_BS, f"Waiting for replyContains: {timeSince} (needs {waitTime})")
return
r = self.regex.search(story.currentReply.getText())
if r is None:
return
@ -756,7 +770,6 @@ class Diversion(object):
return
r = self.regex.search(story.currentReply.getText())
print('repeat?', r)
if r is None:
return
@ -1535,4 +1548,19 @@ class Story(object):
return self.strands[msg.id]
return self.calculateFinishesForMsg(msg.id)
def getDefaultDirectionForMsg(self, msg):
"""
There is only a default direction (for reply contains diversion) if it has
one, and only one, direction to go. If there's more, it should do nothing.
"""
if not msg.id in self.directionsPerMsg:
# is finish
return None
if len(self.directionsPerMsg[msg.id]) > 1:
return None
# TODO: should the direction have at least a timeout condition set, or not perse?
return self.directionsPerMsg[msg.id][0]

View File

@ -1,6 +1,6 @@
apt-get update
apt-get install -y munin-node bc supervisor libsox-fmt-pulse
apt-get install -y munin-node bc supervisor
cp installation/rpi-internal-temp /usr/share/munin/plugins
ln -sf /usr/share/munin/plugins/rpi-internal-temp /etc/munin/plugins/rpi-internal-temp
rm /etc/munin/plugins/irqstats

View File

@ -7,7 +7,7 @@ voice:
port: 4444
chunk: 2972
google_credentials: "../test_googlespeech/My First Project-0c7833e0d5fa.json"
hugveys: 25
hugveys: 26
languages:
- code: en-GB
file: story_en.json
@ -28,7 +28,7 @@ languages:
ms_lang: "fr-FR"
web:
port: 8888
files_dir: "local/"
files_dir: "local/"
light:
ip: "192.168.178.15"
port: 7400
port: 7400

View File

@ -14,6 +14,7 @@ class Panopticon {
hugveys: [],
selectedId: null,
logbook: "",
logbookId: null,
},
methods: {
time_passed: function( hugvey, property ) {
@ -60,6 +61,12 @@ class Panopticon {
hv.status = "loading";
return panopticon.change_language(hv.id, lang_code);
},
change_light: function(e) {
let hv_id = parseInt(e.target.dataset.hvid);
let light_id = parseInt(e.target.value);
console.log(hv_id, light_id, this);
return panopticon.change_light_id(hv_id, light_id);
},
showHugvey: function(hv) {
panopticon.hugveys.selectedId = hv.language ? hv.id : null;
panopticon.hugveys.logbook = [];
@ -222,6 +229,10 @@ class Panopticon {
change_language( hv_id, lang_code ) {
this.send( { action: 'change_language', hugvey: hv_id, lang_code: lang_code } );
}
change_light_id( hv_id, light_id ) {
console.log("Light", hv_id, light_id);
this.send( { action: 'change_light', hugvey: hv_id, light_id: light_id } );
}
playFromSelected(msg_id) {
if(!this.hugveys.selectedId) {
@ -366,6 +377,7 @@ class Graph {
div['params']['returnAfterStrand'] = true;
div['params']['msgId'] = "";
div['params']['notForColor'] = "";
div['params']['waitTime'] = 1.8;
}
else if(type == 'interrupt') {
div['params']['msgId'] = "";
@ -574,6 +586,16 @@ class Graph {
'change': (e) => div['params']['msgId'] = e.target.value
}}, ...msgOptions)
),
crel('label', 'Wait time',
crel('input', {
'type': 'number',
'step': 0.1,
'value': div['params']['waitTime'],
'on': {
'change': (e) => div['params']['waitTime'] = parseFloat(e.target.value)
}
})
),
notAfterMsgIdEl
));
}

View File

@ -18,60 +18,60 @@ class Timeline{
{content: '.', start: new Date(), type: 'point', group: 1}
]);
console.log('init timeline');
let groups = [];
for(let hid = 1; hid<=this.count; hid++) {
groups.push({id: parseInt(hid), content: 'Hugvey #'+hid});
this.eventDataSet.add({content: 'initiate', start: new Date(), type: 'point', group: parseInt(hid)})
}
let dataGroups = new vis.DataSet(groups);
let options = {
// 'rollingMode': {'follow': true, 'offset': .8 }
};
console.log('groups', dataGroups, groups, options);
this.timeline = new vis.Timeline(this.el, this.eventDataSet, dataGroups, options);
let tl = this.timeline;
let startDate = new Date();
startDate.setMinutes(startDate.getMinutes()-1);
let endDate = new Date();
endDate.setMinutes(endDate.getMinutes()+20);
setTimeout(function(){
tl.setWindow(startDate, endDate);
tl.setWindow(startDate, endDate);
}, 500);
this.moveInterval = setInterval(function(){
// skip movement if not visible
tl.moveTo(new Date());
}, 1000);
ws.addEventListener( 'message', this);
}
handleEvent(e) {
console.log('handle', e, this);
if(e.type == 'message') {
this.wsOnMessage(e)
}
}
wsOnMessage(e) {
let msg = JSON.parse( e.data );
if ( typeof msg['action'] === 'undefined' ) {
console.error( "not a valid message: " + e.data );
return;
}
if(msg['action'] != 'log') {
return;
}
console.debug(msg, this);
let hv_id = parseInt(msg['id']);
// {'action': 'log', 'id':hugvey_id, 'type': items[0], 'info', 'args'}
let d, parts;
@ -91,7 +91,7 @@ class Timeline{
this.eventDataSet.update(d);
console.log('update', d);
} else {
this.eventDataSet.add({id: mId, content: msgContent, title: `${msgContent} (${msgId})`, start: new Date(), group: hv_id, 'className': 'message'});
this.eventDataSet.add({id: mId, content: msgContent, title: `${msgContent} (${msgId})`, start: new Date(), group: hv_id, 'className': 'message'});
}
break;
case 'speaking':
@ -101,7 +101,7 @@ class Timeline{
let id = parts.shift();
let content = parts.join(' ');
let scId = 'sc-'+id+'-'+hv_id;
if(info.startsWith('start')){
this.eventDataSet.add({content: info, start: new Date(), type: 'point', group: hv_id, 'className': 'speech'});
}
@ -115,7 +115,7 @@ class Timeline{
this.eventDataSet.update(d);
} else {
console.log('add');
this.eventDataSet.add({id: scId, content: content, title: content, start: new Date(), group: hv_id, 'className': 'speech'});
this.eventDataSet.add({id: scId, content: content, title: content, start: new Date(), group: hv_id, 'className': 'speech'});
}
}
if(info.startsWith('end')){
@ -125,7 +125,7 @@ class Timeline{
this.eventDataSet.update(d);
}
}
break;
case 'story':
// 'info': 'start'/'finished'
@ -147,4 +147,4 @@ class Timeline{
}
}
var tl = new Timeline(ws, document.getElementById('line'), 25);
var tl = new Timeline(ws, document.getElementById('line'), 26);

View File

@ -39,6 +39,9 @@
</option>
</select>
{{ hv.language }}
<!-- <div v-if="hv.awaiting != false"><img class='icon' :src="'/images/icon-finished.svg'" title="Finished"> {{timer(hv,
'finished')}}</div> -->
<div class='stats'>
@ -61,9 +64,10 @@
<div class='btn' v-if="hv.status == 'running'" @click.stop="finish(hv)">Finish</div> <!-- to available state -->
<div class='btn' v-if="hv.status == 'running'" @click.stop="pause(hv)">Pause</div>
<div class='btn' v-if="hv.status == 'paused'" @click.stop="resume(hv)">Resume</div>
<div class='light'>
<!-- <div class='light'>
{{ hv.light }}
</div>
</div> -->
<div class='light'>Light: <input type="number" step="1" :value="hv.light_id" @change="change_light" :data-hvid="hv.id" v-on:click.stop></div>
</div>
</div>
</div>
@ -72,7 +76,7 @@
<h1>Log of {{logbookId}}</h1>
<div v-for="log in logbook" class='log'>
<div class='time'>{{formatted(log.time)}}</div>
<div class='content {{log.origin}}'>
<div :class="['content', log.origin]">
<span class='origin'>{{log.origin}}</span>
<span class='msg'>{{log.msg}}</span>
<span v-if="log.extra" class='extra'>( {{log.extra}} )</span>