#!/bin/sh
# shellcheck disable=SC3043,SC3040
# Script for locking a system in Eichrecht mode
#
#  Copyright (c) 2020, Ebee Smart Technologies GmbH

export PATH=/opt/ebee/bin:/opt/ebee/sbin:/opt/ebee/usr/bin:/opt/ebee/usr/sbin:/opt/ebee/usr/local/bin:/opt/ebee/usr/local/sbin:"$PATH"

params=$*

set -e
set -o pipefail

# busybox's find dooes not support -printf "%f" so we need to use this
find_print_base()
{
	find "$@" -exec basename '{}' \;
}

if [ -d /opt/ebee/usr/share/er_lock_data ] ; then
	ER_LOCK_DATA_DIR=/opt/ebee/usr/share/er_lock_data
else
	ER_LOCK_DATA_DIR="$(dirname "$(readlink -f "$0")")/er_lock_data"
fi

CHARGE_USER=charge
CHARGE_GROUP=charge
ER_LOCK_KEY_DIR="${ER_LOCK_DATA_DIR}/keys"
_PROD_PUBLIC_KEY="${ER_LOCK_KEY_DIR}/sw_update_20230731.pub"
if [ -e "$_PROD_PUBLIC_KEY" ] ; then
	DEFAULT_PUBLIC_KEY="$_PROD_PUBLIC_KEY"
else
	DEFAULT_PUBLIC_KEY=/home/eichrecht/sw_update.pub
fi
SYSTEM_HASH_CMD=/home/eichrecht/system_hash
S00PERMISSIONS_CMD=/etc/init.d/S00permissions
ER2_SUPPORTED_CMD=er2_supported

FAKED_FUNCTIONS=

fake_evmd_meter_for_er204=true

legacy_version()
{
	echo "WARNING: using version number from /tmp/.ebee_version.txt, this may be wrong" >&2
	cat /tmp/.ebee_version.txt
}

# First, try to get the version from the OPKG database.
opkg_content_version() {
	local V
	V="$(/home/eichrecht/ebee_opkg --conf /home/eichrecht/ebee_opkg.conf info ebee-inner-eichrecht-2-0-update | grep 'Version:' | cut -d" " -f 2)"
	if [ -z "$V" ] ; then
		return 1
	else
		echo "$V"
	fi
}

# Then try the /log/flash.ver.
flash_content_version()
{
	# TODO: we are not checking the OPKG database because I did not find a way
	# to test it. The problem will arise in a fleshly flashed 5.3x system and
	# in a system where the eichrecht sw was upgraded using opkg.
	json-extract -sr -i /log/flash.ver sys_ver
}

# If we have an old system whose inner eichrecht was not updated, we need to guess.
guess_legacy_content_version()
{
	echo "WARNING: trying to guess version number from file contents" >&2
	local GUESS_FILE="/home/eichrecht/evmd"
	local GUESS_F_SHA
	GUESS_F_SHA="$(sha256sum "$GUESS_FILE" | cut -d" " -f 1)"
	case "$GUESS_F_SHA" in
		"48427d333de41c7be11acf11fc6f1d23cd8db1094cc62d4acf7443e021acccae")
			echo "5.23.0-13679"
			;;
		*)
			return 1
			;;
	esac
}

controller_sw_version()
{
	# TODO: we are not checking the OPKG database because I did not find a way
	# to test it. The problem will arise in a fleshly flashed 5.3x system and
	# in a system where the eichrecht sw was upgraded using opkg.
	grep "-" /tmp/.ebee_version.txt
}

need_er_data()
{
	local EBEE_REV
	EBEE_REV="$( (controller_sw_version || legacy_version) | sed -n 's/[0-9]\+\.\([0-9]\+\)\..\+/\1/p')"

	[ "$EBEE_REV" -ge 31 ]
}

er_data_version()
{
	need_er_data && cat /opt/ebee/usr/share/er/artifacts.ver
}

tstamp() {
	date -Iseconds
}

fatal () {
	printf "%s \033[1;91;107mFATAL: %s\033[0m\n" "$(tstamp)" "$@"
	logger "er_lock (ERROR): $@"
	exit 1
}

info () {
	printf "%s \033[1;94;107m%s\033[0m\n" "$(tstamp)" "$@"
	logger "er_lock (INFO): $@"
}

WITH_SCHUKO="n"

check_user()
{
	[ "$(whoami)" = "root" ] || fatal "must be run by root"
}

check_rcmb_flash_supported()
{
	info "Checking RCMB"
	for i in $(seq 3); do
		$ER2_SUPPORTED_CMD 2>/dev/null && return
	done
}

check_system_compat()
{
	check_rcmb_flash_supported || fatal "RCMB: Eichrecht 2.0 not supported"

	[ ! -e /etc/eichrecht/eichrecht.locked ] || fatal "system already seems to be locked"

	! need_er_data || [ -f /opt/ebee/usr/share/er/artifacts.tar.xz ] || \
		fatal "missing lock-data, please install ER_data.deb"

	[ -e /dev/ubi0 ] || fatal "system can't be made Eichrecht-compliant"
}

check_sw_settings()
{
	grep -q '^Modbus Eichrecht$' /home/charge/persistency/Config_meter 2>/dev/null || \
		fatal "'Modbus Eichrecht' not configured as OCPP meter in application"
}

check_system_time()
{
	year=$(date | awk '{print $6}')
	[ "$year" -ge 2020 ] || fatal "system time not valid"
}

#-----------------------------------------------------------------
# Check that all conditions are satisfied that are needed for
# locking the system for real
#
check_prerequisites_dev()
{
	info "Checking prerequisites"

	check_user
	check_system_compat
	check_sw_settings
	check_system_time
}

check_prerequisites_user()
{
	info "Checking prerequisites"
	check_user
	check_system_compat
	check_system_time
}

#-----------------------------------------------------------------
# Make sure to enter a clean state before locking
#

EVMD_PRIVKEYS_DIR="/home/eichrecht/private_keys"

ensure_clean_state_before_locking()
{
    if [ -f $EVMD_PRIVKEYS_DIR/log_key.priv ] || [ -f $EVMD_PRIVKEYS_DIR/private_key ]; then
        rm -f $EVMD_PRIVKEYS_DIR/*
    fi
}

#-----------------------------------------------------------------
# Prints out usage information
#

print_usage_dev()
{
	cat >&2 <<EOF
Usage: er_lock ARGS
Locks down a system to make it Eichrecht compliant.

Mandatory arguments:
  -c,--capsule_id   'Messkapsel-Id', a non-empty string
  -l,--loss_factor  cable loss factor, a positive integer between 1 and 1000
  -p,--public_key   public key for inner SW updates, can be either a file
                    or a string with the public key encoded as a hex string
                    or with base64
  -m,--meter_type   meter type / hw config

Supported meter types:

EOF
	for F in "$TYPE_FILE_DIR"/*.conf.json ; do
		local confbase
		confbase="$(basename "$F")"
		echo "  ${confbase%%.conf.json}" >&2
	done

	cat >&2 <<EOF

	'gossen' is an alias for 'em2289-mnnk'
	'dzg' is an alias for 'dvh4013'

Optional argument:
  -s,--schuko        if specified, the target will be locked for Schuko use
  -h,--hash          target hash as a 64 char long hex string,
                     if not given the target hash of the system
                     will be calculated and printed out
  -r,--relative      Enable relative timestamps in OCMF ("R" flag, see spec).
  --calc_hash        if this option is present, then a value for the hash is not
                     required, and it will be set to the calculated value.
  -w,--workaround    Trigger hardware-specific workarounds
     Supported workarounds:
        dwh_as_dvh   Use the DZG DWH4113 with an eichrecht version that only
                     supports DVH4013.
  -e,--exec          Execute only the specified action, one of:
                       test_args: check arguments and exit
                       show_codes: show codes for config and version (used to
                         find config files and hash files).
                       make_config: create config files for evmd. Can run on the
                         target or on the host.
                       evmd: configure evmd
                       mark_locked: mark system as locked for Eichrecht.
                       sshd: set sshd to run as charge
                       perms: remove suid bits and capabilities
                       lock_all: lock system and reboot (default)
  --host-sim           Skip/replace hardware dependent checks and use alternate
                       utilities. This is intended for use on a host system to
                       simulate the locking process and predict the resulting
                       hash. Effects:
                       - EVMD is not invoked, nor meter-cli.
                       - evm-log is not invoked, and lock files are created
                         by this script.
                       - S00permisions and system_hash are searched for in /tmp
                         first, then in the usual locations.
EOF
}

print_usage_user()
{
	cat >&2 <<EOF
To lock using the given configuration code:
	er_lock [-p <pub key>] <configuration code> <capsule_id>

Other commands:
	er_lock help           # show this help
	er_lock version_code   # show version code for for the current system.
	er_lock list           # list all supported configuration codes
	er_lock check          # check preconditions and compatibility
	er_lock check [-p <pub key>] <configuration code> <capsule_id>  # check argument

Configuration code format is made up of the following dot-separated parts:

	<meter type>.L<loss factor>[.I or R][.S or Sn]

	Example 1: Gossen.L1000 -> Gossen meter, 1000 loss factor, defaults to
			   informative timestamps (I) and no Schuko (Sn)

	Example 2: DZG4113.L995.R.S -> DZG4113 meter, 995 loss factor, relative
			   timestamps (R) and Schuko (S)

 <meter type> is one of the following:
EOF
	find_print_base "$TYPE_FILE_DIR" -type f -name '*.conf.json' | sed 's/\(.\+\)\.conf\.json$/  \1/'
	cat >&2 <<EOF
 The last two fields are optional:
  - I or R: I for informative timestamps, R for relative timestamps, if absent
			defaults to I
  - S or Sn: S for Schuko, Sn for no Schuko, if absent defaults to Sn

Use 'er_lock list' to list all supported configuration codes.
EOF
}

fatal_usage()
{
	printf "\033[1;91;107mWRONG ARGUMENTS: %s\033[0m\n" "$@"
	logger "er_lock (USAGE): $@"
	print_usage
	exit 1
}

lock_command() {
	evm-log lock
}

# Do the same as evm-log lock, but without the having access to the hardware
lock_command_fake() {
	mkdir -p /etc/eichrecht
	chown root:eichrecht  /etc/eichrecht
    chmod ug=rwx,o=rx     /etc/eichrecht

	echo 2 > /etc/eichrecht/eichrecht.locked
	chown root:root /etc/eichrecht/eichrecht.locked
	chmod ugo=r     /etc/eichrecht/eichrecht.locked

	touch /etc/eichrecht/eichrecht.clean
	chown eichrecht:eichrecht /etc/eichrecht/eichrecht.clean
	chmod ug=rw,o=r /etc/eichrecht/eichrecht.clean
}

FAKED_FUNCTIONS=$FAKED_FUNCTIONS" lock_command"

#-----------------------------------------------------------------
# Writes the first entry into the Eichrect log and creates
# the 'eichrecht.locked' and # 'eichrecht.clean' files
# in '/etc/eichrecht'.
#
mark_system_locked_and_clean()
{
	info "Marking system as locked for Eichrecht"

	# If locking the system fails re-enable root shell and
	# passwords for root and eichrecht etc.

	if ! lock_command
	then
		printf "\033[1;91;107mLocking failed, trying to undo changes...\033[0m\n"

		sed -i 's/^root:\(.*\)\/bin\/false/root:\1\/bin\/sh/' /etc/passwd || :
		/etc/init.d/S31password_sync start || :
        echo "su = ssx" >> /etc/busybox.conf || :
		ln -sf /etc/init.d/charge/S50dropbear /etc/init.d/S50dropbear 2> /dev/null || :
		exit 1
	fi
}

# This code depends on the configuration parameters selected, but not on the
# sofware version or the hardware. It is used to generate the name of the
# directory where the full configuration is stored.
make_config_code()
{
	local RELATIVE_CODE
	if [ "$RELATIVE_TS" = "true" ] ; then
		RELATIVE_CODE=R
	else
		RELATIVE_CODE=I
	fi

	local WITH_SCHUKO_CODE
	if [ "$WITH_SCHUKO" = "y" ] ; then
		WITH_SCHUKO_CODE=S
	else
		WITH_SCHUKO_CODE=Sn
	fi

	printf "%s.L%d.%s.%s" "$METER_TYPE" "$LOSS_FACTOR" "$RELATIVE_CODE" "$WITH_SCHUKO_CODE"
}

variant_base()
{
	sed -n 's/\([a-zA-Z0-9]\+\).*/\1/p' < /dev/ebee_variant
}

# Try to get the version in different ways, in order of preference.
get_frozen_version()
{
	er_data_version \
	  || opkg_content_version \
	  || flash_content_version \
	  || guess_legacy_content_version \
	  || legacy_version
}

# This code is a combination of the software and hardware version. It is used
# to generate the name of the file where the hash is stored.
make_version_code()
{
	local FROZEN_VER
	local VARIANT_BASE
	FROZEN_VER="$(get_frozen_version)"
	VARIANT_BASE="$(variant_base)"
	printf "%s:%s\n" "$FROZEN_VER" "$VARIANT_BASE"
}

show_codes()
{
	make_config_code && echo
	local VCODE
	if VCODE="$(make_version_code 2>/dev/null)" ; then
		echo "$VCODE"
	fi
}

evmd_restart()
{
    info "Restarting evmd"
    /etc/init.d/eichrecht/S97evmd restart
}

evmd_stop()
{
    info "Stopping evmd"
    /etc/init.d/eichrecht/S97evmd stop
}

evmd_start()
{
    info "Starting evmd"
    /etc/init.d/eichrecht/S97evmd start
}

stop_ebee_app()
{
    info "Stopping controller software"
    touch /home/charge/stop_ebee

    ebee_pid=$(pidof ebee_cp_plus_application_stripped) || true
    if [ ! -z $ebee_pid ]; then
        for i in $(seq 7); do
            kill $ebee_pid
            sleep 1
            ebee_pid=$(pidof ebee_cp_plus_application_stripped) || true
            if [ -z $ebee_pid ]; then
                break
            fi
        done
    fi

    ebee_pid=$(pidof ebee_cp_plus_application_stripped) || true
    if [ ! -z $ebee_pid ]; then
        kill -9 $ebee_pid
    fi
}

#------------------------------------------------------------------
# Write device-specific configuration data for evmd.
#

EVMD_CONF_DIR=/home/eichrecht
EVMD_CONF_DIR_ON_TARGET=/home/eichrecht

: "${EVMD_SOCKET_FILE:="/tmp/evmd/evmd.sock"}"
: "${EVMD_DEVICE_CONF_FILE:="device.conf.json"}"
: "${EVMD_USER:="eichrecht"}"
: "${EVMD_GROUP:=$EVMD_USER}"

set_evmd_device_params()
{
	info "Setting evmd device parameters, including serial number"

	local F="${EVMD_CONF_DIR}/${EVMD_DEVICE_CONF_FILE}"

	_METER_SERIAL_NUM="$(get_meter_serial)"
	json-merge -c - "$F" <<DEVICE_PARAMS_HERE
{
    "meter": {
        "meter_serial": "${_METER_SERIAL_NUM}"
    },
    "eichrecht": {
        "capsule_id": "$CAPSULE_ID"
    }
}
DEVICE_PARAMS_HERE

	if [ -n "$BENCHMARK_RELAY" ] ; then
		json-merge -i "$F" - <<DEVICE_PARAMS_HERE
{
	"session": {"benchmark_relay": {"file": "$BENCHMARK_RELAY", "use_ioctl": false}}
}
DEVICE_PARAMS_HERE
	fi
}


#------------------------------------------------------------------
# Write type-specific configuration data for evmd.
#

if [ -d "${ER_LOCK_DATA_DIR}/base.configs" ] ; then
	TYPE_FILE_DIR="${ER_LOCK_DATA_DIR}/base.configs"
else
	TYPE_FILE_DIR=/home/eichrecht/evmd.configs
fi

FULL_CONFIG_DIR="${ER_LOCK_DATA_DIR}/full.configs"

: "${EVMD_TYPE_CONF_FILE:="type.conf.json"}"
: "${EVMD_EICHRECHT_TARGET_HASH_FILE:="eichrecht.hash"}"
: "${EVMD_EICHRECHT_KEY_DIR:="private_keys"}"
: "${EVMD_USER:="eichrecht"}"
: "${EVMD_GROUP:=$EVMD_USER}"

set_evmd_type_params()
{
	info "Setting evmd type parameters"

	local F="${EVMD_CONF_DIR}/${EVMD_TYPE_CONF_FILE}"

	# FIXME: the private key and the hash should use a relative path, but we
	# can't change how the config is generated for older systems or that will
	# change the hash.
	json-merge -i "$F" - <<TYPE_PARAMS_HERE
{
    "meter": {
        "loss_compensation": $LOSS_FACTOR
    },
    "eichrecht": {
        "sys_hash": "${EVMD_CONF_DIR_ON_TARGET}/${EVMD_EICHRECHT_TARGET_HASH_FILE}",
        "private_key": "${EVMD_CONF_DIR_ON_TARGET}/${EVMD_EICHRECHT_KEY_DIR}/private_key"
    },
    "relay": {
        "parallel": {
            "type2": {"file": "/dev/ebee_powerctrl", "use_ioctl": false}
        }
    }
}
TYPE_PARAMS_HERE

	if [ "$RELATIVE_TS" = "true" ] ; then
		json-merge -i "$F" - <<TYPE_PARAMS_HERE
{
	"session": {"legal_timing": true}
}
TYPE_PARAMS_HERE
	fi

    if [ $WITH_SCHUKO = "y" ]
    then
        json-merge -i "$F" - <<TYPE_PARAMS_HERE
{
    "relay": {
        "parallel": {
            "schuko": {"file": "/dev/ebee_schukoctrl", "use_ioctl": false}
        }
    }
}
TYPE_PARAMS_HERE
    fi
}

METER_CLI_SESSION=/tmp/meter-cli.log

get_meter_serial()
{
	sed -ne 's/Serial: //p' < "$METER_CLI_SESSION"
}

get_meter_serial_fake()
{
	echo "123456789"
}

FAKED_FUNCTIONS=$FAKED_FUNCTIONS" get_meter_serial"

set_evmd_conf_permissions()
{
	# FIXME: make type.conf.json owned by user eichrecht instead of root
	chown root:"$EVMD_GROUP" "${EVMD_CONF_DIR}/${EVMD_TYPE_CONF_FILE}"
	chmod ugo=r "${EVMD_CONF_DIR}/${EVMD_TYPE_CONF_FILE}"
	chown "$EVMD_USER":"$EVMD_GROUP" "${EVMD_CONF_DIR}/${EVMD_DEVICE_CONF_FILE}"
	chmod u=rw,go=r "${EVMD_CONF_DIR}/${EVMD_DEVICE_CONF_FILE}"
}

#-----------------------------------------------------------------
# Make sure that the ssh daemon (dropbear) is run by the 'charge'
# user by removing the symlink /etc/init.d/S50dropbear. It pointed
# to the script in /etc/init.d/charge of the same name which will
# now be used to start dropbear as user 'charge' instead of 'root'.
#

run_sshd_as_charge()
{
	info "Running ssh daemon as user 'charge'"
	rm /etc/init.d/S50dropbear 2> /dev/null || :
}


#-----------------------------------------------------------------
# Update the file with the public key which is used to check the
# signatures of "inner Eichrecht" updates
#

set_public_key()
{
	info "Setting public SW update key"

	mv "$PUBLIC_KEY" /home/eichrecht/sw_update.pub
	chown eichrecht:eichrecht /home/eichrecht/sw_update.pub
	chmod ug=rw,o=r           /home/eichrecht/sw_update.pub
}


#-----------------------------------------------------------------
# Goes through all directories on the system (except those
# that get removed on reboot) and removes all capabilities
# and suid/sgid bits from regular, executable files.
purge_system()
{
	info "System-wide removal of suid bits and capabilities"
	find  / -xdev -type f -perm +u=s,g=s -exec chmod u-s,g-s {} \;
	find  / -xdev -type f -perm +u=x,g=x,o=x -exec setcap -r {} \; 2>/dev/null
}


#-----------------------------------------------------------------
# For the 'root' and 'eichrecht' account remove the password from
# /etc/shadow (replacing it by '*', disallowing log-ins). Also
# remove the log-in shell for 'root'.
# Wipes out the '/root' directory and remove an '.ssh' directory
# from /home/eichrecht, which could contain an 'authorized_keys'
# file.
# Note: 'eichrecht must be able to inspect the (empty) /root
# directory as it runs 'system_hash' and thus must hash the
# contents of that directory.
#
# Also note: the lines for root and eichrecht in /etc/shadow become
# included into the system hash. Thus we can't use just 'passwd -l'
# to disable the accounts, as that only prepends the existing password
# with a '!', thus making the hash contain the previously set password,
# To avoid that we first have to wipe the password with 'passwd -d' be-
# fore disabling it with 'passwd -l'.

disable_accounts()
{
	info "Disabling 'root' and 'eichrecht' accounts"

	sed -i 's/^root:\(.*\)\/bin\/sh/root:\1\/bin\/false/' /etc/passwd
	passwd -d root > /dev/null
	passwd -l root > /dev/null

	rm -rf /root 2> /dev/null
	mkdir /root
	chown root:eichrecht /root
	chmod u=rwx,g=rx,o=  /root

	passwd -d eichrecht > /dev/null || :
	passwd -l eichrecht > /dev/null || :
	rm -rf /home/eichrecht/.ssh 2> /dev/null
}

# Note: the file must be writable by 'eichrect' as its
# content may have to be changed during an inner Eichrecht
# update.
#
set_target_hash_permissions()
{
	chown eichrecht:eichrecht "$1"
	chmod ug=rw,o=r           "$1"
}

set_target_hash()
{
	# Note: evmd only accepts a file with a single, new-line (i.e. 0x0a)
	# terminiated line. File must be modifiable by 'eichrecht' as the
	# hash can change (and then must be replaced) by Eichrecht updates
	local F="${EVMD_CONF_DIR}/${EVMD_EICHRECHT_TARGET_HASH_FILE}"

	echo "$TARGET_HASH" >     "$F"
	set_target_hash_permissions "$F"
}


#-----------------------------------------------------------------
# Calculates and sets the target hash
#-----------------------------------------------------------------

calc_and_set_target_hash()
{
	# Permissions and ownership (but not the content) of the target hash file
	# go into the hash, so create a dummy file before starting to calculate
	# the hash value

	TARGET_HASH="83aa29f088e4ee89a28e9858c8d072f755b611ace819f48beea928474a0d8f68"
	set_target_hash

	# Set up everything as it would after a reboot of a locked system

	$S00PERMISSIONS_CMD start

	# Calculate and then write out the target hash

	info "Calculating target hash"
	TARGET_HASH=$($SYSTEM_HASH_CMD -v 2> /log/target_hash.log)
	echo "${TARGET_HASH}" >> /log/target_hash.log
	check_hash
	set_target_hash

	info "Target hash is '$TARGET_HASH'"
}


#-----------------------------------------------------------------
# Checks the target hash argument which must be a 64 character
# long hex string
#

check_hash()
{
	[ -n "$(echo "$TARGET_HASH" | sed 's/ //g')" ] || fatal_usage "missing or empty target hash"

	# evmd expects the hash in lower case
	TARGET_HASH=$(echo "$TARGET_HASH" | tr "ABCDEF" "abcdef")

	(echo "$TARGET_HASH" | grep -Eq '^[a-f0-9]{64}$') || \
		fatal_usage "target hash must be a 64 char long hex string"
}


#------------------------------------------------------------------
# Checks the capsule ID argument
#

check_capsule_id()
{
	[ -n "$(echo "$CAPSULE_ID" | sed 's/ //g')" ] || fatal_usage "missing or empty capsule ID"
}


#------------------------------------------------------------------
# Tests the loss factor argument which must be a positive integer
#

check_loss_factor()
{
	[ -n "$(echo "$LOSS_FACTOR" | sed 's/ //g')" ] || fatal_usage "missing or empty loss factor"
	(echo "$LOSS_FACTOR" | grep -Eq '^[0-9]+$') || fatal_usage "loss factor isn't a positive integer"
}

# ----------------------------------------------------------------
# Trigger meter compatibility workarounds
#
prepare_meter()
{
    info "Preparing meter"
	local type_file
	type_file="${EVMD_CONF_DIR}/${EVMD_TYPE_CONF_FILE}"

	local TTY
	TTY=$(json-extract -rs meter rtu device <"$type_file")
	local BAUD
	BAUD=$(json-extract -d meter rtu baud <"$type_file" || echo 9600)
	local RTS
	RTS=$(json-extract -rs meter rtu rts <"$type_file" || echo "none")
	local EVENP
	EVENP=$(json-extract -b meter rtu even_parity <"$type_file" || echo false)
	local MODEL
	MODEL=$(json-extract -rs meter model <"$type_file" || echo em2289 )

    # workaround the fact that DWH4113 in ER-2.0.4 is addressed as a DVH4013
    # see issue #1959
    er_ver=$(cat /opt/ebee/usr/share/er/artifacts.ver)
    case $er_ver in
        "5.23.0-13679")
            case $MODEL in
                "dwh4113")
                    info "Fixing ER-2.0.4 DHW4113 naming"
                    sed -i "s/dwh4113/dvh4013/g" $type_file
                    # need to have meter-cli and evmd unpacked before trying to
                    # find and lock for the meter in ER-2.0.4 fir the DHW4113
                    xzcat /opt/ebee/usr/share/er/artifacts.tar.xz | tar -xv -C / opt/ebee/usr/bin/meter-cli home/eichrecht/evmd >/dev/null
                    MODEL="dvh4013" ;;
                *) ;;
            esac ;;
        *) ;;
    esac

	local SLAVEID
	SLAVEID=$(json-extract -d meter slave_id <"$type_file" || echo 1 )

	local PARITY
	if "$EVENP" ; then
		PARITY='E'
	else
		PARITY='N'
	fi

	if [ "$RTS" = "none" ] ; then
		RTS=""
	fi

	local ACTUAL_MODEL
	if [ "$MODEL" = "dvh4013" ] && [ "$WORKAROUNDS" = "dwh_as_dvh" ] ; then
		ACTUAL_MODEL="dwh4113"
	else
		ACTUAL_MODEL="$MODEL"
	fi

	info "Prepare meter: $ACTUAL_MODEL at $TTY:8${PARITY}1:$BAUD:$SLAVEID"

	( meter-cli -n "fragile on" \
		"modbus_open 1 $TTY $PARITY $BAUD $RTS" \
		"modbus_meter 1 1 $ACTUAL_MODEL $SLAVEID" \
		"write 1 display/bl white display/special 1 display/flags i display/info Preparing" \
		"sleep 1500" \
		"write 1 display/info done" \
		"sleep 500" | tee "$METER_CLI_SESSION" ) || fatal "meter communication error"

	if grep -q "Model: DVH4013" $METER_CLI_SESSION; then
		if [ "$MODEL" != "dvh4013" ]; then
			fatal "attached meter is not a DZG-DVH4013"
		fi
	elif grep -q "Model: DWH4113" $METER_CLI_SESSION; then
		if [ "$MODEL" != "dwh4113" ]; then
			fatal "attached meter is not a DZG-DWH4113"
		elif grep -q "fw_version: V1.06" $METER_CLI_SESSION; then
			fatal "meter DZG-DWH4113.x with FW-Version V1.06 is revoked and unsupported"
		fi
	fi
}

# Check if the meter selection is valid and normalize the name (i.e. if it is an
# alias, replace it with the actual name)
# Note that a more appropriate name instead of "meter type" would be "base
# configuration" since it contains more than just the meter type, but the
# name is kept for historical reasons.
check_meter_type_and_normalize()
{
	case "$METER_TYPE" in
		gossen)
			METER_TYPE="em2289-mnnk"
			;;
		dzg)
			METER_TYPE="dvh4013"
			;;
		dvh4113)
			METER_TYPE="dvh4013-2.19"
			;;
	esac

	[ -n "$(echo "$METER_TYPE" | sed 's/ //g')" ] || fatal_usage "missing or empty meter type"

	local type_file="${TYPE_FILE_DIR}/${METER_TYPE}.conf.json"
	local real_type_file

	real_type_file=$(readlink -f "$type_file")

	[ -e "$real_type_file" ] || fatal_usage "invalid meter type: $METER_TYPE"
	METER_TYPE=$(basename "$real_type_file" .conf.json)

	echo "$real_type_file"
}

apply_meter_type()
{
	local type_file
	type_file=$(check_meter_type_and_normalize)
	local F="${EVMD_CONF_DIR}/${EVMD_TYPE_CONF_FILE}"
	rm -f "$F" # remove default symlink
	cp "$type_file" "$F"
}


#-----------------------------------------------------------------
# Checks the public key argument and. It can be either a
# (readable) file or a string with the (binary) public key
# either as a hex string or base64 encoded. In the latter
# cases convert the string back to binary and write it into
# a temporary file.
#

check_public_key()
{
	[ -n "$(echo "$PUBLIC_KEY" | sed 's/ //g')" ] || fatal_usage "missing or empty public key argument"

	if [ -f "$PUBLIC_KEY" ]
	then
		[ -r "$PUBLIC_KEY" ] || fatal "file '$PUBLIC_KEY' given as public key argument can't be read"

		# Make a temporary copy of the public key file - it may reside in
		# /root which gets deleted.

		tmp=$(mktemp)
		cp "$PUBLIC_KEY" "$tmp"
		PUBLIC_KEY=$tmp
		return
	fi

	# Check if the argument could be a hex string. If yes copy it into a
	# temporary file to be later copied to the correct place.

	tmp=$(mktemp)

	if hexdec "$PUBLIC_KEY" > "$tmp" 2>/dev/null
	then
		PUBLIC_KEY=$tmp
		return
	fi

	# Check if the argument could be a base64 encoded string

	if base64dec "$PUBLIC_KEY" > "$tmp" 2>/dev/null
	then
		PUBLIC_KEY="$tmp"
		return
	fi

	fatal_usage "invalid public key"
}

# Check that the system can be locked and the parameters are right
check_all()
{
	check_prerequisites_dev

	# We may be called without a target hash, in which case we're supposed
	# to calculate it after having set up everything needed for locking.

	if [ "$CALC_HASH" != "true" ]
	then
		check_hash
	fi

	check_capsule_id
	check_loss_factor
	check_meter_type_and_normalize
	check_public_key
}

configure_evmd()
{
	check_loss_factor

	evmd_stop
	apply_meter_type
	prepare_meter
	set_evmd_type_params
	set_evmd_device_params
	set_evmd_conf_permissions
}

# Like configure_evmd, but does not actually invoke evmd or meter-cli.
# This just needs to ensure that the configuration files are in place.
# it assumes that evmd is not running.
configure_evmd_fake()
{
	check_loss_factor

	apply_meter_type
	set_evmd_type_params
	set_evmd_device_params
	set_evmd_conf_permissions
}

FAKED_FUNCTIONS=$FAKED_FUNCTIONS" configure_evmd"

# Produce the same type.conf that would be produced by running the script in
# a live system.
make_evmd_type_conf()
{
	check_loss_factor
	check_meter_type_and_normalize

	EVMD_CONF_DIR="${ER_LOCK_DATA_DIR}/full.configs/$(make_config_code)"
	mkdir -p "$EVMD_CONF_DIR"
	echo "$params" > "$EVMD_CONF_DIR/params"
	apply_meter_type
	set_evmd_type_params
}

set_schuko_grabber()
{
	# shellcheck disable=SC2155
	local SCHUKO_FILE="$(json-extract -rs relay parallel schuko file -i "${EVMD_CONF_DIR}/${EVMD_TYPE_CONF_FILE}")"
	if [ -z "$SCHUKO_FILE" ] ; then
		return
	fi
	cat << GRAB_SCHUKO_GPIO > /etc/init.d/S01lock_schuko_gpio
#!/bin/sh
#
# Locks the GPIO used for Schuko relay for the ebee_powerctrl module
#

case "\$1" in
	start)
		echo -n '0' > $SCHUKO_FILE
        ;;
	stop)
		;;
	restart|reload)
		;;
	*)
		echo "Usage: \$0 {start|stop|restart}"
		exit 1
		;;
esac
GRAB_SCHUKO_GPIO
	chown root.root   /etc/init.d/S01lock_schuko_gpio
	chmod u=rwx,go=rx /etc/init.d/S01lock_schuko_gpio
}

unpack_artifacts()
{
	info "Processing Eichrecht artifacts"
	# unpack locked content and run some cleanups on the filesystem
	if [ -f /opt/ebee/usr/share/er/artifacts.tar.xz ]; then
		xzcat /opt/ebee/usr/share/er/artifacts.tar.xz | tar -xv -C / >/dev/null
		rm -f /opt/ebee/usr/share/er/artifacts.tar.xz
		/opt/ebee/usr/share/er/clean_artifacts_general.sh
	fi

	# fixes for the bootloader being flashed all the time because the
	# cmp on the target (busybox) does not support the "-n" option;
	# on locked systems, we copy the "bad" check_bootloader script using
	# the "-n" option (to keep the hash compat) and install the reboot-script
	# including a workaround to /opt/ebee;
	if [ -f /opt/ebee/usr/share/er/opt_ebee_sbin_reboot_hwwatch ]; then
		cp -a /opt/ebee/usr/share/er/opt_ebee_sbin_reboot_hwwatch /opt/ebee/sbin/reboot
	fi
	if [ -f /opt/ebee/usr/share/er/usr_sbin_check_bootloader_cmpn ]; then
		cp -a /opt/ebee/usr/share/er/usr_sbin_check_bootloader_cmpn /usr/sbin/check_bootloader
	fi
}

evmd_start_fake()
{
	info "Starting evmd (skipped in simulation mode)"
}

FAKED_FUNCTIONS=$FAKED_FUNCTIONS" evmd_start"

system_modifications()
{
	# evmd is needed by evm-log
	evmd_start

	run_sshd_as_charge
	purge_system
	disable_accounts
	set_public_key
	mark_system_locked_and_clean

	unpack_artifacts

	set_schuko_grabber
}

autodetect_relay_file()
{
	case "$(cat /dev/ebee_variant)" in
		CC612*)
			echo /sys/class/gpio/pioA18/value
			;;
		CC613*)
			echo /sys/class/gpio/pioB24/value
			;;
		*)
			fatal "unable to detect controller model for relay selection"
			;;
	esac
}

save_configs()
{
	local SAVE_DIR
	SAVE_DIR="/home/charge/er_lock_out/$(make_config_code)"

	mkdir -p "$SAVE_DIR"
	chmod 775 "$SAVE_DIR"
	cp "${EVMD_CONF_DIR}/${EVMD_TYPE_CONF_FILE}" "$SAVE_DIR"
	cp "${EVMD_CONF_DIR}/${EVMD_EICHRECHT_TARGET_HASH_FILE}" "$SAVE_DIR/$(make_version_code).hash"
	cp /log/target_hash.log "$SAVE_DIR/$(make_version_code).hash.log"
	chmod 664 "$SAVE_DIR"/*
	chown -R "$CHARGE_USER:$CHARGE_GROUP" "$SAVE_DIR"
}

do_reboot()
{
	info "rebooting..."
	sync
	reboot
}

do_reboot_fake()
{
	info "reboot (skipped in sim mode)"
}

FAKED_FUNCTIONS=$FAKED_FUNCTIONS" do_reboot"

#-----------------------------------------------------------------
# Does everything required to bring the system into the Eichrecht
# compliant state and then immediately reboots it.
#
lock_all()
{
	check_all

	# We better shut down the Ebee app before proceeding - evmd may still
	# be needed in the first steps, so shut it down only afterwards
	stop_ebee_app

	# Now the fun starts for real

	info "Locking system for eichrecht"

	configure_evmd

	system_modifications

	# If no target hash argument was given calculate it. Then install
	# it and reboot, finalizing the locking of the system.

	if [ "$CALC_HASH" = "true" ]
	then
		calc_and_set_target_hash
	else
		set_target_hash
	fi

	info "Successfully locked system for Eichrecht, saving config files..."

	save_configs

	do_reboot
}

# Replace some functions "f" with "f_fake" which do not require hardware
replace_functions_with_fakes() {
	echo "Replacing functions with fakes"
	for f in $FAKED_FUNCTIONS; do
		eval "$f() { ${f}_fake \"\$@\"; }"
	done

	if [ -e /tmp/hash_scripts/S00permissions ]; then
		echo "Replacing S00permisions with fake"
		S00PERMISSIONS_CMD=/tmp/hash_scripts/S00permissions
	fi
	if [ -e /tmp/hash_scripts/system_hash ]; then
		echo "Replacing system_hash with fake"
		SYSTEM_HASH_CMD=/tmp/hash_scripts/system_hash
	fi
	ER2_SUPPORTED_CMD=true
}

dev_mode() {
	print_usage() { print_usage_dev; }
	# If called with no arguments show usage information

	[ $# -ne 0 ] || { print_usage ; exit 0; }

	# Set default action (in case there's no '-e' or '--exec' option)

	exec="lock_all";

	# Make sure the variables we expect to receive aren't accidentally
	# already set by an environment variable.

	TARGET_HASH=""
	CAPSULE_ID=""
	LOSS_FACTOR=""
	PUBLIC_KEY="${DEFAULT_PUBLIC_KEY}"
	METER_TYPE=""
	WORKAROUNDS=""
	BENCHMARK_RELAY=""
	RELATIVE_TS=""
	CALC_HASH="false"
	HOST_SIM="false"

	# Evaluate command line arguments

	sopts="h:c:l:m:p:e:m:w:b:rs"
	lopts="hash:,capsule_id:,loss_factor:,meter_type:,public_key:,exec:,workaround:,benchmark:,relative,schuko,calc_hash,host-sim"

	if ! getopt -o $sopts -l $lopts -n "er_lock" -- "$@" >/dev/null
	then
		print_usage
	fi

	OPTS=$(getopt -o $sopts -l $lopts -n "er_lock" -- "$@")
	eval set -- "$OPTS"

	while [ -n "$1" ] && [ "$1" != "--" ]
	do
		shifts=2
		case "$1" in
			-h|--hash)
				TARGET_HASH="$2"
				;;
			--calc_hash)
				shifts=1
				CALC_HASH="true"
				;;
			-c|--capsule_id)
				CAPSULE_ID="$2"
				;;
			-l|--loss_factor)
				LOSS_FACTOR="$2"
				;;
			-m|--meter_type)
				METER_TYPE="$2"
				;;
			-p|--public_key)
				PUBLIC_KEY="$2"
				;;
			-e|--exec)
				exec="$2"
				;;
			-w|--workaround)
				WORKAROUNDS="$2"
				;;
			-r|--relative)
				shifts=1
				RELATIVE_TS="true"
				;;
			-s|--schuko)
				shifts=1
				WITH_SCHUKO="y"
				;;
			--host-sim)
				shifts=1
				HOST_SIM="true"
				;;
		esac

		shift $shifts
	done

	if [ "$HOST_SIM" = "true" ] ; then
		replace_functions_with_fakes
	fi

	BENCHMARK_RELAY="$(autodetect_relay_file)"

	case "$exec" in
		test_args)
			check_all

			echo "Hash:        $TARGET_HASH"
			echo "Capsule ID:  $CAPSULE_ID"
			echo "Loss factor: $LOSS_FACTOR"
			echo "Public Key:  $PUBLIC_KEY"
			echo "Meter Type:  $METER_TYPE"
			echo "HW Work-arounds: $WORKAROUNDS"
			;;
		show_codes)
			show_codes
			;;
		make_config)
			make_evmd_type_conf
			;;
		evmd)
			configure_evmd
			;;
		mark_locked)
			mark_system_locked_and_clean
			;;
		sshd)
			run_sshd_as_charge
			;;
		perms)
			purge_system
			$S00PERMISSIONS_CMD start
			;;
		lock_all)
			trap "fatal 'unexpected error'" EXIT
			echo "params: '" "$params" "'" > /log/er_lock.log
			lock_all 2>&1 | tee -a /log/er_lock.log
			trap - EXIT
			;;
		*)
			fatal_usage "invalid command '$exec'"
	esac
}

_normalize_config_code() {
	# we are given the fields of the config code as argument and we need to
	# complete the optional fields with the default values if they are missing
	# The possibilities for what we may get are
	# 1. we get all four fields
	# 2. we get only the first two fields
	# 3. we get only the first three fields
	# 4. we get only the first two fields plus the last one (which is will be
	#    in position 3)
	local default_timestamp="I"
	local default_schuko="Sn"

	if [ $# -eq 4 ] ; then
		echo "$1.$2.$3.$4"
	elif [ $# -eq 2 ] ; then
		echo "$1.$2.$default_timestamp.$default_schuko"
	elif [ $# -eq 3 ] ; then
		# if the third field is a timestamp then we have case 3, otherwise
		# we have case 2
		if [ "$3" = "I" ] || [ "$3" = "R" ] ; then
			echo "$1.$2.$3.$default_schuko"
		else
			echo "$1.$2.$default_timestamp.$3"
		fi
	else
		return 1
	fi
}

normalize_config_code() {
	# shellcheck disable=SC2046
	_normalize_config_code $(echo "$1" | tr '.' ' ')
}

check_user_args()
{
	check_prerequisites_user

    ensure_clean_state_before_locking

	if [ "$1" = '-p' ] ; then
		if [ -z "$2" ] ; then
			fatal_usage "missing public key argument"
		fi
		PUBLIC_KEY="$2"
		shift 2
	else
		PUBLIC_KEY=$DEFAULT_PUBLIC_KEY
	fi

	if [ $# -ne 2 ] ; then
		fatal_usage "invalid number of arguments"
	fi

	local config_code_canonical
	if ! config_code_canonical="$(normalize_config_code "$1")" ; then
		fatal_usage "invalid config code '$1'"
	fi

	EVMD_TYPE_CONF="${FULL_CONFIG_DIR}/$config_code_canonical/type.conf.json"
	local VERSION_CODE
	VERSION_CODE=$(make_version_code)
	HASH_FILE="${FULL_CONFIG_DIR}/$config_code_canonical/${VERSION_CODE}.hash"
	CAPSULE_ID="$2"

	[ -f "$EVMD_TYPE_CONF" ] || fatal_usage "invalid config code '$1' -> '$config_code_canonical'"
	[ -f "$HASH_FILE" ] || fatal_usage "invalid version code '$VERSION_CODE'"
	check_public_key
	check_capsule_id

	BENCHMARK_RELAY="$(autodetect_relay_file)"
}

apply_evmd_type_conf()
{
	rm -f "${EVMD_CONF_DIR}/${EVMD_TYPE_CONF_FILE}"

	# NOTE mpi: for er-2.0.3 (5.21) we should not add the relay section
	#           since the evmd in this version did not support it yet
	VERCODE=$(get_frozen_version)
	case $VERCODE in
		*5.21.*) cp -L "${EVMD_TYPE_CONF}.203" "${EVMD_CONF_DIR}/${EVMD_TYPE_CONF_FILE}" ;;
		*) cp -L "$EVMD_TYPE_CONF" "${EVMD_CONF_DIR}/${EVMD_TYPE_CONF_FILE}" ;;
	esac

	cp -L "$HASH_FILE" "${EVMD_CONF_DIR}/${EVMD_EICHRECHT_TARGET_HASH_FILE}"
	set_target_hash_permissions "${EVMD_CONF_DIR}/${EVMD_EICHRECHT_TARGET_HASH_FILE}"
}

evmd_config_user()
{
	info "Configuring evmd"
	evmd_stop
	apply_evmd_type_conf
	prepare_meter
	set_evmd_device_params
	set_evmd_conf_permissions
}

set_ebee_app_meter_config()
{
	info "Setting ebee app meter to Modbus Eichrecht"
	printf "Modbus Eichrecht\n13725 3405ffa72c" >/home/charge/persistency/Config_meter
}

lock_all_user()
{
	info "Locking system for eichrecht"
	stop_ebee_app
	set_ebee_app_meter_config
	evmd_config_user
	system_modifications
	info "Successfully locked system for Eichrecht..."
	do_reboot
}

list_configs()
{
	local config_codes
	config_codes=$(find_print_base "$FULL_CONFIG_DIR" -mindepth 1 -maxdepth 1 -type d | sort)
	for CF in $config_codes ; do
		echo "$CF"
		local versions
		versions=$(find_print_base "${FULL_CONFIG_DIR}/${CF}" -mindepth 1 -maxdepth 1 -type f -name '*.hash' | sort)
		for V in $versions ; do
			printf "  %s\t" "${V%.hash}"
			cat "${FULL_CONFIG_DIR}/${CF}/${V}"
		done
	done
}

user_mode() {
	print_usage() { print_usage_user; }
	if [ $# -eq 0 ] || [ "$1" = help ] ; then
		print_usage
		return
	fi

	if [ "$1" = "version_code" ] ; then
		make_version_code
		return
	fi

	if [ "$1" = "list" ] ; then
		list_configs
		return
	fi

	local CHECK_ONLY=false

	if [ "$1" = "check" ] ; then
		CHECK_ONLY=true
		shift
	fi

	check_user_args "$@"
	if [ "$CHECK_ONLY" = "true" ] ; then
		return
	fi

	trap "fatal 'unexpected error'" EXIT
	echo "params: '" "$params" "'" > /log/er_lock.log
	lock_all_user 2>&1 | tee -a /log/er_lock.log
	trap - EXIT
}

main() {
	local THIS_COMMAND
	THIS_COMMAND=$(basename "$0")
	if [ "$THIS_COMMAND" = er_lock_dev ] ; then
		dev_mode "$@"
	elif [ "$THIS_COMMAND" = er_lock ] ; then
		user_mode "$@"
	fi
}

main "$@"
